From 49bd3c22f36da4bb44996dbd64c7113b7abd705d Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi <35780660+anakrish@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:43:19 -0600 Subject: [PATCH] feat!: add Rego Virtual Machine (RVM) implementation (#495) * feat!: add Rego Virtual Machine (RVM) implementation This commit introduces a register-based virtual machine for executing Rego policies with bytecode-style instructions. Unlike the existing tree-walking interpreter, the RVM compiles policies into instruction sequences that operate on virtual registers, offering better performance and optimization potential. Core Components: Instruction Set Architecture: - Define instruction types for data operations, control flow, and builtins - Implement instruction parameter encoding and display formatting - Add instruction parser with comprehensive test coverage Virtual Machine Engine: - Register-based execution model with program counter management - Loop execution supporting iterators, comprehensions, and quantifiers - Function call handling with argument evaluation and context management - Rule evaluation with default value resolution and virtual data support - Arithmetic and comparison operation implementations Program Representation: - Program listing builder with instruction sequencing - Rule tree construction for organizing policy rules - Binary and JSON serialization for compiled programs - Recompilation support for program modification Testing Infrastructure: - Extensive YAML test suites covering all VM features - Rust unit tests for VM execution and instruction parsing - Test suites for loops, comprehensions, builtins, and control flow BREAKING CHANGE: Introduces new VM execution path alongside interpreter Signed-off-by: Anand Krishnamoorthi * docs: add detailed RVM architecture references Introduce architecture.md explaining program artifacts, serialization, and runtime subsystems. Document the full opcode catalog in instruction-set.md, including operands, parameter tables, and outcomes. Walk through execution flow, stacks, and operational guidance in vm-runtime.md, tying the runtime to the new architecture docs. Signed-off-by: Anand Krishnamoorthi --------- Signed-off-by: Anand Krishnamoorthi --- Cargo.lock | 128 +-- Cargo.toml | 9 +- bindings/ffi/Cargo.lock | 108 +- bindings/java/Cargo.lock | 100 +- bindings/python/Cargo.lock | 100 +- bindings/wasm/Cargo.lock | 100 +- docs/rvm/architecture.md | 274 +++++ docs/rvm/instruction-set.md | 238 +++++ docs/rvm/vm-runtime.md | 243 +++++ scripts/pre-push | 2 +- src/lib.rs | 2 + src/rvm/instructions/display.rs | 280 +++++ src/rvm/instructions/mod.rs | 381 +++++++ src/rvm/instructions/params.rs | 445 ++++++++ src/rvm/instructions/types.rs | 47 + src/rvm/mod.rs | 16 + src/rvm/program/core.rs | 322 ++++++ src/rvm/program/listing.rs | 964 +++++++++++++++++ src/rvm/program/mod.rs | 19 + src/rvm/program/recompile.rs | 22 + src/rvm/program/rule_tree.rs | 105 ++ src/rvm/program/serialization/binary.rs | 482 +++++++++ src/rvm/program/serialization/json.rs | 196 ++++ src/rvm/program/serialization/mod.rs | 29 + src/rvm/program/serialization/value.rs | 294 ++++++ src/rvm/program/types.rs | 182 ++++ src/rvm/tests/instruction_parser.rs | 648 ++++++++++++ src/rvm/tests/mod.rs | 12 + src/rvm/tests/test_utils.rs | 104 ++ src/rvm/tests/vm.rs | 995 ++++++++++++++++++ src/rvm/vm/arithmetic.rs | 101 ++ src/rvm/vm/comprehension.rs | 543 ++++++++++ src/rvm/vm/context.rs | 97 ++ src/rvm/vm/dispatch.rs | 771 ++++++++++++++ src/rvm/vm/errors.rs | 121 +++ src/rvm/vm/execution.rs | 591 +++++++++++ src/rvm/vm/execution_model.rs | 179 ++++ src/rvm/vm/functions.rs | 74 ++ src/rvm/vm/loops.rs | 661 ++++++++++++ src/rvm/vm/machine.rs | 325 ++++++ src/rvm/vm/mod.rs | 23 + src/rvm/vm/rules.rs | 627 +++++++++++ src/rvm/vm/state.rs | 107 ++ src/rvm/vm/virtual_data.rs | 293 ++++++ src/tests/mod.rs | 2 +- src/value.rs | 17 +- tests/rvm/vm/README.md | 78 ++ .../rvm/vm/suites/arithmetic_operations.yaml | 168 +++ tests/rvm/vm/suites/assertions.yaml | 47 + tests/rvm/vm/suites/basic_instructions.yaml | 32 + tests/rvm/vm/suites/boolean_literals.yaml | 44 + tests/rvm/vm/suites/builtin_functions.yaml | 524 +++++++++ tests/rvm/vm/suites/call_rule.yaml | 76 ++ .../rvm/vm/suites/comparison_operations.yaml | 111 ++ tests/rvm/vm/suites/complex.yaml | 230 ++++ .../vm/suites/constructed_collections.yaml | 80 ++ tests/rvm/vm/suites/control_flow.yaml | 115 ++ tests/rvm/vm/suites/core_semantics.yaml | 232 ++++ tests/rvm/vm/suites/data_structures.yaml | 76 ++ tests/rvm/vm/suites/deep_nesting.yaml | 278 +++++ tests/rvm/vm/suites/default_rules.yaml | 277 +++++ tests/rvm/vm/suites/destructuring_rules.yaml | 41 + tests/rvm/vm/suites/function_calls.yaml | 371 +++++++ tests/rvm/vm/suites/halt.yaml | 14 + tests/rvm/vm/suites/host_await.yaml | 137 +++ tests/rvm/vm/suites/host_await_failures.yaml | 28 + tests/rvm/vm/suites/indexed_access.yaml | 241 +++++ .../rvm/vm/suites/integration_scenarios.yaml | 432 ++++++++ .../interpreter_operator_compatibility.yaml | 351 ++++++ .../rvm/vm/suites/invalid_collection_ops.yaml | 61 ++ tests/rvm/vm/suites/load_data_input.yaml | 250 +++++ .../rvm/vm/suites/loop_invalid_iteration.yaml | 43 + .../vm/suites/loops/array_comprehensions.yaml | 160 +++ tests/rvm/vm/suites/loops/empty.yaml | 295 ++++++ tests/rvm/vm/suites/loops/existential.yaml | 174 +++ .../loop_comprehension_interactions.yaml | 223 ++++ tests/rvm/vm/suites/loops/nested.yaml | 183 ++++ tests/rvm/vm/suites/loops/nested_fixed.yaml | 7 + .../suites/loops/object_comprehensions.yaml | 192 ++++ .../vm/suites/loops/set_comprehensions.yaml | 139 +++ tests/rvm/vm/suites/loops/universal.yaml | 143 +++ .../vm/suites/null_undefined_handling.yaml | 376 +++++++ tests/rvm/vm/suites/object_operations.yaml | 276 +++++ tests/rvm/vm/suites/predefined.yaml | 165 +++ tests/rvm/vm/suites/resource_limits.yaml | 217 ++++ tests/rvm/vm/suites/serialization.yaml | 423 ++++++++ tests/rvm/vm/suites/set_operations.yaml | 284 +++++ tests/rvm/vm/suites/type_errors.yaml | 224 ++++ tests/rvm/vm/suites/virtual_data_lookup.yaml | 274 +++++ 89 files changed, 19158 insertions(+), 313 deletions(-) create mode 100644 docs/rvm/architecture.md create mode 100644 docs/rvm/instruction-set.md create mode 100644 docs/rvm/vm-runtime.md create mode 100644 src/rvm/instructions/display.rs create mode 100644 src/rvm/instructions/mod.rs create mode 100644 src/rvm/instructions/params.rs create mode 100644 src/rvm/instructions/types.rs create mode 100644 src/rvm/mod.rs create mode 100644 src/rvm/program/core.rs create mode 100644 src/rvm/program/listing.rs create mode 100644 src/rvm/program/mod.rs create mode 100644 src/rvm/program/recompile.rs create mode 100644 src/rvm/program/rule_tree.rs create mode 100644 src/rvm/program/serialization/binary.rs create mode 100644 src/rvm/program/serialization/json.rs create mode 100644 src/rvm/program/serialization/mod.rs create mode 100644 src/rvm/program/serialization/value.rs create mode 100644 src/rvm/program/types.rs create mode 100644 src/rvm/tests/instruction_parser.rs create mode 100644 src/rvm/tests/mod.rs create mode 100644 src/rvm/tests/test_utils.rs create mode 100644 src/rvm/tests/vm.rs create mode 100644 src/rvm/vm/arithmetic.rs create mode 100644 src/rvm/vm/comprehension.rs create mode 100644 src/rvm/vm/context.rs create mode 100644 src/rvm/vm/dispatch.rs create mode 100644 src/rvm/vm/errors.rs create mode 100644 src/rvm/vm/execution.rs create mode 100644 src/rvm/vm/execution_model.rs create mode 100644 src/rvm/vm/functions.rs create mode 100644 src/rvm/vm/loops.rs create mode 100644 src/rvm/vm/machine.rs create mode 100644 src/rvm/vm/mod.rs create mode 100644 src/rvm/vm/rules.rs create mode 100644 src/rvm/vm/state.rs create mode 100644 src/rvm/vm/virtual_data.rs create mode 100644 tests/rvm/vm/README.md create mode 100644 tests/rvm/vm/suites/arithmetic_operations.yaml create mode 100644 tests/rvm/vm/suites/assertions.yaml create mode 100644 tests/rvm/vm/suites/basic_instructions.yaml create mode 100644 tests/rvm/vm/suites/boolean_literals.yaml create mode 100644 tests/rvm/vm/suites/builtin_functions.yaml create mode 100644 tests/rvm/vm/suites/call_rule.yaml create mode 100644 tests/rvm/vm/suites/comparison_operations.yaml create mode 100644 tests/rvm/vm/suites/complex.yaml create mode 100644 tests/rvm/vm/suites/constructed_collections.yaml create mode 100644 tests/rvm/vm/suites/control_flow.yaml create mode 100644 tests/rvm/vm/suites/core_semantics.yaml create mode 100644 tests/rvm/vm/suites/data_structures.yaml create mode 100644 tests/rvm/vm/suites/deep_nesting.yaml create mode 100644 tests/rvm/vm/suites/default_rules.yaml create mode 100644 tests/rvm/vm/suites/destructuring_rules.yaml create mode 100644 tests/rvm/vm/suites/function_calls.yaml create mode 100644 tests/rvm/vm/suites/halt.yaml create mode 100644 tests/rvm/vm/suites/host_await.yaml create mode 100644 tests/rvm/vm/suites/host_await_failures.yaml create mode 100644 tests/rvm/vm/suites/indexed_access.yaml create mode 100644 tests/rvm/vm/suites/integration_scenarios.yaml create mode 100644 tests/rvm/vm/suites/interpreter_operator_compatibility.yaml create mode 100644 tests/rvm/vm/suites/invalid_collection_ops.yaml create mode 100644 tests/rvm/vm/suites/load_data_input.yaml create mode 100644 tests/rvm/vm/suites/loop_invalid_iteration.yaml create mode 100644 tests/rvm/vm/suites/loops/array_comprehensions.yaml create mode 100644 tests/rvm/vm/suites/loops/empty.yaml create mode 100644 tests/rvm/vm/suites/loops/existential.yaml create mode 100644 tests/rvm/vm/suites/loops/loop_comprehension_interactions.yaml create mode 100644 tests/rvm/vm/suites/loops/nested.yaml create mode 100644 tests/rvm/vm/suites/loops/nested_fixed.yaml create mode 100644 tests/rvm/vm/suites/loops/object_comprehensions.yaml create mode 100644 tests/rvm/vm/suites/loops/set_comprehensions.yaml create mode 100644 tests/rvm/vm/suites/loops/universal.yaml create mode 100644 tests/rvm/vm/suites/null_undefined_handling.yaml create mode 100644 tests/rvm/vm/suites/object_operations.yaml create mode 100644 tests/rvm/vm/suites/predefined.yaml create mode 100644 tests/rvm/vm/suites/resource_limits.yaml create mode 100644 tests/rvm/vm/suites/serialization.yaml create mode 100644 tests/rvm/vm/suites/set_operations.yaml create mode 100644 tests/rvm/vm/suites/type_errors.yaml create mode 100644 tests/rvm/vm/suites/virtual_data_lookup.yaml diff --git a/Cargo.lock b/Cargo.lock index e774d73..f88f90f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,9 +18,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -108,6 +108,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "serde", + "unty", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -131,9 +141,9 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "borrow-or-share" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" [[package]] name = "bstr" @@ -165,9 +175,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.43" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", "shlex", @@ -231,9 +241,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.50" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", "clap_derive", @@ -241,9 +251,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.50" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstream", "anstyle", @@ -543,9 +553,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -556,9 +566,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -569,11 +579,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -584,42 +593,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -656,6 +661,8 @@ checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", + "serde", + "serde_core", ] [[package]] @@ -738,9 +745,9 @@ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" @@ -982,9 +989,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -1175,6 +1182,7 @@ name = "regorus" version = "0.5.0" dependencies = [ "anyhow", + "bincode", "cfg-if", "chrono", "chrono-tz", @@ -1183,6 +1191,7 @@ dependencies = [ "dashmap", "data-encoding", "globset", + "indexmap", "ipnet", "jsonschema", "lazy_static", @@ -1230,20 +1239,6 @@ name = "scientific" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38a4b339a8de779ecb098a772ecbba2ace74e23ed959a5b4f30631d8bf1799a8" -dependencies = [ - "scientific-macro", -] - -[[package]] -name = "scientific-macro" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ee4885492bb655bfa05d039cd9163eb8fe9f79ddebf00ca23a1637510c2fd2" -dependencies = [ - "proc-macro2 1.0.103", - "quote 1.0.41", - "syn 2.0.108", -] [[package]] name = "scopeguard" @@ -1416,9 +1411,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -1460,9 +1455,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-xid" @@ -1476,6 +1471,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.7" @@ -1777,9 +1778,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xtask" @@ -1794,11 +1795,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -1806,9 +1806,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2 1.0.103", "quote 1.0.41", @@ -1859,9 +1859,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -1870,9 +1870,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -1881,9 +1881,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2 1.0.103", "quote 1.0.41", diff --git a/Cargo.toml b/Cargo.toml index e545aa2..f2113bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ keywords = ["interpreter", "no_std", "opa", "policy-as-code", "rego"] doctest = false [features] -default = ["full-opa", "arc"] +default = ["full-opa", "arc", "rvm"] arc = ["scientific/arc"] ast = [] @@ -38,6 +38,7 @@ net = ["dep:ipnet"] no_std = ["lazy_static/spin_no_std"] opa-runtime = [] regex = ["dep:regex"] +rvm = ["dep:bincode", "dep:indexmap"] semver = ["dep:semver"] std = ["rand/std", "rand/std_rng", "serde_json/std", "msvc_spectre_libs" ] time = ["dep:chrono", "dep:chrono-tz"] @@ -99,7 +100,7 @@ lazy_static = { version = "1.4.0", default-features = false } thiserror = { version = "2.0", default-features = false } data-encoding = { version = "2.8.0", optional = true, default-features=false, features = ["alloc"] } -scientific = { version = "0.5.3" } +scientific = { version = "0.5.3", default-features = false } globset = { version = "0.4.16", features = ["simd-accel"], default-features = false, optional = true } regex = {version = "1.11.1", optional = true, default-features = false } @@ -120,6 +121,10 @@ msvc_spectre_libs = { version = "0.1", features = ["error"], optional = true } dashmap = { version = "6.1", default-features = false, optional = true } mimalloc = { path = "mimalloc", optional = true } +# rvm related deps +indexmap = { version = "2.12.0", default-features = false, features = ["serde"], optional = true } +bincode = { version = "2.0.1", default-features = false, features = ["alloc", "serde"], optional = true } + [dev-dependencies] anyhow = "1.0.45" cfg-if = "1.0.0" diff --git a/bindings/ffi/Cargo.lock b/bindings/ffi/Cargo.lock index 0019dea..94ed006 100644 --- a/bindings/ffi/Cargo.lock +++ b/bindings/ffi/Cargo.lock @@ -18,9 +18,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -125,9 +125,9 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "borrow-or-share" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" [[package]] name = "bstr" @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.43" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", "shlex", @@ -211,18 +211,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.50" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.50" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstream", "anstyle", @@ -431,9 +431,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -444,9 +444,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -457,11 +457,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -472,42 +471,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -620,9 +615,9 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" @@ -814,9 +809,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -1022,20 +1017,6 @@ name = "scientific" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38a4b339a8de779ecb098a772ecbba2ace74e23ed959a5b4f30631d8bf1799a8" -dependencies = [ - "scientific-macro", -] - -[[package]] -name = "scientific-macro" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ee4885492bb655bfa05d039cd9163eb8fe9f79ddebf00ca23a1637510c2fd2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] [[package]] name = "scopeguard" @@ -1201,9 +1182,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -1252,9 +1233,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unsafe-libyaml" @@ -1534,17 +1515,16 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -1552,9 +1532,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -1605,9 +1585,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -1616,9 +1596,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -1627,9 +1607,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", diff --git a/bindings/java/Cargo.lock b/bindings/java/Cargo.lock index c5041a3..2c7d819 100644 --- a/bindings/java/Cargo.lock +++ b/bindings/java/Cargo.lock @@ -18,9 +18,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -75,9 +75,9 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "borrow-or-share" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" [[package]] name = "bstr" @@ -109,9 +109,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.43" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", "shlex", @@ -303,9 +303,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -316,9 +316,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -329,11 +329,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -344,42 +343,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -502,9 +497,9 @@ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" @@ -690,9 +685,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -893,20 +888,6 @@ name = "scientific" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38a4b339a8de779ecb098a772ecbba2ace74e23ed959a5b4f30631d8bf1799a8" -dependencies = [ - "scientific-macro", -] - -[[package]] -name = "scientific-macro" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ee4885492bb655bfa05d039cd9163eb8fe9f79ddebf00ca23a1637510c2fd2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] [[package]] name = "scopeguard" @@ -1064,9 +1045,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -1074,9 +1055,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unsafe-libyaml" @@ -1352,17 +1333,16 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -1370,9 +1350,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -1423,9 +1403,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -1434,9 +1414,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -1445,9 +1425,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", diff --git a/bindings/python/Cargo.lock b/bindings/python/Cargo.lock index 9e0314e..7211506 100644 --- a/bindings/python/Cargo.lock +++ b/bindings/python/Cargo.lock @@ -18,9 +18,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -75,9 +75,9 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "borrow-or-share" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" [[package]] name = "bstr" @@ -103,9 +103,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "cc" -version = "1.2.43" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", "shlex", @@ -287,9 +287,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -300,9 +300,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -313,11 +313,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -328,42 +327,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -473,9 +468,9 @@ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" @@ -685,9 +680,9 @@ checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -944,20 +939,6 @@ name = "scientific" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38a4b339a8de779ecb098a772ecbba2ace74e23ed959a5b4f30631d8bf1799a8" -dependencies = [ - "scientific-macro", -] - -[[package]] -name = "scientific-macro" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ee4885492bb655bfa05d039cd9163eb8fe9f79ddebf00ca23a1637510c2fd2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] [[package]] name = "scopeguard" @@ -1101,9 +1082,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -1111,9 +1092,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unindent" @@ -1301,17 +1282,16 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -1319,9 +1299,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -1372,9 +1352,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -1383,9 +1363,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -1394,9 +1374,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", diff --git a/bindings/wasm/Cargo.lock b/bindings/wasm/Cargo.lock index c0bb965..b02f09c 100644 --- a/bindings/wasm/Cargo.lock +++ b/bindings/wasm/Cargo.lock @@ -18,9 +18,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -75,9 +75,9 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "borrow-or-share" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" [[package]] name = "bstr" @@ -103,9 +103,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "cc" -version = "1.2.43" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", "shlex", @@ -296,9 +296,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -309,9 +309,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -322,11 +322,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -337,42 +336,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -473,9 +468,9 @@ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" @@ -671,9 +666,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -877,20 +872,6 @@ name = "scientific" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38a4b339a8de779ecb098a772ecbba2ace74e23ed959a5b4f30631d8bf1799a8" -dependencies = [ - "scientific-macro", -] - -[[package]] -name = "scientific-macro" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ee4885492bb655bfa05d039cd9163eb8fe9f79ddebf00ca23a1637510c2fd2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] [[package]] name = "scopeguard" @@ -1028,9 +1009,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -1038,9 +1019,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unsafe-libyaml" @@ -1303,17 +1284,16 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -1321,9 +1301,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -1374,9 +1354,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -1385,9 +1365,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -1396,9 +1376,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", diff --git a/docs/rvm/architecture.md b/docs/rvm/architecture.md new file mode 100644 index 0000000..f76f5c9 --- /dev/null +++ b/docs/rvm/architecture.md @@ -0,0 +1,274 @@ +# Regorus Virtual Machine Architecture + +This document explains how Rego source becomes executable bytecode and how the +runtime evaluates it. It is meant for three audiences: + +- **Engine developers** working on the RVM execution core and runtime subsystems. +- **Policy front-end authors** targeting the VM from alternate policy languages. +- **Operators/tools** wanting to reason about execution behaviour and + troubleshooting output. + + +The high-level pipeline looks like this: + +``` + ┌───────────┐ emit Program ┌────────────┐ load & run ┌─────────┐ + │ Parser & │ ───────────────▶ │ Program │ ─────────────▶ │ Rego VM │ + │ Compiler │ (bytecode) │ Artifact │ (instructions│ Runtime │ + └───────────┘ │ │ + metadata) │ │ + └────────────┘ └─────────┘ +``` + +Each step feeds the next via well-defined data structures described below. + +--- + +## RVM in context + +The Rego VM uses a register-based architecture with the following traits: + +- **Register windows per frame**: Each rule or function call receives a + compile-time-sized register window. Windows are pooled and reused to keep the + runtime allocation profile predictable. +- **Sequential bytecode stream**: Fixed-width 32-bit instructions execute from + a linear program counter with optional jumps. Complex instructions reference + shared tables (`InstructionData`) that carry literals, loop metadata and call + parameters. +- **Literal and builtin tables**: Literal pools and builtin dispatch tables are + resolved at load time so bytecode stays compact and symbol lookups remain + constant-time during execution. +- **Extended control stacks**: Loop, rule-cache and comprehension stacks sit + alongside the core call stack, enabling suspension, short-circuiting and + deterministic rule caching without growing the register windows themselves. + +--- + +## 1. Compilation Outputs + +A successful compilation produces a `Program` (`src/rvm/program/core.rs`). The +layout is deliberately split: + +- **Stable artifact section**: Always serialised and treated as canonical. It + captures the original policy sources, entry-points, compiler options, + etc. +- **Synthesised execution section**: It contains the + compiled instruction stream, instruction parameter tables, literal tables, etc. + It can be recreated from the stable artifact section if a future RVM version is + note able to deserialize it. + + +| Field | Purpose | Notes | +| :---------------------------------------------- | :--------------------------------------------------------------- | :---- | +| `instructions: Vec` | Ordered bytecode emitted by the compiler. | Each opcode is defined in `src/rvm/instructions/mod.rs` and executed by the dispatch tree. | +| `literals: Vec` | Literal constants shared across instructions. | Skipped by serde but written in the binary format via `BinaryValueSlice`; avoids duplicating large value graphs. | +| `instruction_data: InstructionData` | Parameter tables for complex opcodes. | Tables are indexed by `params_index` values stored in instructions. | +| `builtin_info_table: Vec` | Metadata for builtin calls. | Enforced and resolved by `Program::initialize_resolved_builtins`. | +| `entry_points: IndexMap` | Maps path names (e.g. `data.pkg.rule`) to starting PCs. | Preserves declaration order for tooling and serialized in the artifact section. | +| `sources: Vec` | Captures original policy sources. | Stored in the stable artifact section alongside entry-points. | +| `rule_infos: Vec` | Metadata for every rule. | Includes register windows, default values, destructuring blocks. | +| `instruction_spans: Vec>` | Optional span info for diagnostics. | Lines/columns mapped back into the source table when present. | +| `main_entry_point: usize` | Default bytecode entry point. | Used by loaders to jump into the top-level policy. | +| `max_rule_window_size` / `dispatch_window_size` | Register window sizing hints. | The VM uses these to size register banks up-front. | +| `metadata: ProgramMetadata` | Compilation metadata (`compiler_version`, etc.). | Helps operators verify provenance and tooling compatibility. | +| `rule_tree: Value` | Map of rule labels for conflict detection and lookups. | Serialized via `BinaryValueRef`; rebuilt into a `Value::Object` during load. | +| `resolved_builtins: Vec` | Resolved builtin function pointers. | Not serialized; repopulated by the host at load time. | +| `needs_runtime_recursion_check: bool` | Flags when `VirtualDataDocumentLookup` requires runtime guards. | Ensures the VM short-circuits recursion before hitting the instruction budget ceiling. | +| `needs_recompilation: bool` | Indicates partial deserialization of execution data. | Set when the extensible section fails; signals the loader to recompile. | +| `rego_v0: bool` | Records whether the policy targeted Rego v0 semantics. | Ensures recompilation preserves language-version behaviour. | + +Additional helpers such as `Program::add_*`, `Program::update_*`, and +`Program::display_instruction_with_params` are used by the compiler and +inspection tooling to populate and render the program. + +### Serialization layout + +The module `src/rvm/program/serialization` writes `Program` instances into a +compact binary envelope that stays forward-compatible within a major format +version: + +1. **Header**: magic `REGO` bytes followed by `SERIALIZATION_VERSION` (currently + `3`). +2. **Section manifest**: four little-endian `u32` lengths for entry points, + sources, literals, and the rule tree, plus a single-byte `rego_v0` flag. +3. **Preamble payloads**: each section is encoded with `bincode` using helper + wrappers (`BinaryValueSlice`, `BinaryValueRef`) to stream complex `Value` + graphs without cloning. +4. **Program core**: the remaining `Program` struct is serialized once more via + `bincode`; fields skipped by serde (entry points, literals, sources, + rule_tree, resolved builtins) are re-inserted from the preamble when the + program is reconstructed. + +During deserialization the loader sanity-checks the header, lengths, and +version before decoding each preamble section. Any failure while decoding the +core payload downgrades the result to `DeserializationResult::Partial`, +preserving enough artifact data to trigger a recompilation. Successful loads +call `Program::initialize_resolved_builtins` so host runtimes can plug in their +builtin implementations. + +--- + +## 2. Runtime Subsystems + +At evaluation time the `RegoVM` (`src/rvm/vm/machine.rs`) consumes a `Program` +and exposes execution APIs. The VM separates concerns through specialised +stacks and caches. + + +````text +Runtime stacks (run-to-completion) + +┌──────────────────────────── RegoVM ─────────────────────────────┐ +│ Registers (active window) ─────┐ │ +│ Program counter (pc) ───────┐ │ │ +│ ▼ ▼ │ +│ Control flow dispatcher ───────────────▶ Instruction stream │ +│ ▲ ▲ │ +│ Rule cache ────────┐ │ │ Loop stack (LoopContext) │ +│ Evaluation cache │ │ └──▶ Comprehension stack │ +│ Host await queue ──┴─▶ Return values / suspensions │ +└─────────────────────────────────────────────────────────────────┘ + +Suspendable mode frame stack + +┌───────────────────────────────────────────────────────────────────────┐ +│ Frame stack │ +│ │ +│ ┌────────────────┐ ┌────────────────┐ ┌──────────────────────┐ │ +│ │ RuleFrame │ → │ LoopFrame │ → │ ComprehensionFrame │ │ +│ └────────────────┘ └────────────────┘ └──────────────────────┘ │ +│ ▲ ▲ ▲ │ +│ │ push frame │ push frame │ push frame│ +│ ▼ ▼ ▼ │ +│ allow { ... } │ +│ some user in input.users │ +│ [x | ... ] │ +└─────────┴─────────────────────────────────────────────────────────────┘ + +Execution state machine (suspendable) + + ┌──────────────────────┐ + │ Suspended │ + └─────▲────────────┬───┘ + │ │ + | │ + │ │ + │ │ + HostAwait/Breakpoint/Step │ | resume + │ │ + │ │ + | ▼ +┌──────────┐ ┌───────────────────────────┐ Return ┌────────────┐ +│ Ready ├─────────────▶│ Running │────────────▶│ Completed │ +└──────────┘ └────────────┬──────────────┘ └────────────┘ + │ VmError + ▼ + ┌──────────┐ + │ Error │ + └──────────┘ +```` + +Key state: + +- **Registers**: The active register window for the current frame. Windows are + allocated per rule call using a register pool to minimise allocations. +- **Program counter (`pc`)**: The bytecode index for run-to-completion mode. In + suspendable mode, each frame tracks its own `pc`. +- **Rule cache**: Stores results and completion flags per rule to avoid + recomputation. +- **Loop/comprehension stacks**: Track iteration state, completion criteria, and + pending yields. +- **Execution stack**: Present in suspendable mode. Stores `ExecutionFrame` + objects (`FrameKind::Rule`, `Loop`, `Comprehension`) so that the VM can pause + and resume evaluation cleanly. +- **Host await responses**: For run-to-completion execution, pre-defined values + keyed by identifier. Suspendable mode instead returns a + `SuspendReason::HostAwait` to the caller. +- **Evaluation cache**: Used by `VirtualDataDocumentLookup` to memoise path + results. + + +--- + +## 3. Execution Modes + +The VM supports two execution styles selected via `set_execution_mode`. + +### Run-to-completion + +- Entry point: `RegoVM::execute` or `execute_entry_point_by_{index,name}`. +- Control loop: `execute_run_to_completion` → `jump_to` which iterates the + instruction stream sequentially. +- Suspension: Unsupported. Any instruction that would suspend emits a runtime + error because the host cannot resume. +- Traps: Instruction budget enforced via `max_instructions`; exceeding the limit + returns `VmError::InstructionLimitExceeded`. + +### Suspendable + +- Entry point: same as above, but the VM calls `run_stackless_from` which pushes + a main `ExecutionFrame` and dispatches instructions through + `run_stackless_loop`. +- Frames: Each instruction can adjust the currently active frame or push/pop + new frames (rule calls, loops, comprehensions). +- Suspension: `InstructionOutcome::Suspend` transitions the VM into + `ExecutionState::Suspended` with a `SuspendReason` (host await, breakpoint, + single-step). The host must call `resume` with an optional value to continue. +- Breakpoints & step mode: Configured via `set_step_mode` and breakpoint + mutators on `ExecutionState`. Execution halts when a frame `pc` matches a + registered breakpoint. + +In both modes the VM constantly validates safety conditions: parameter indices +must resolve, register windows must exist, and results must stay inside the +supported `Value` lattice. Errors are reported as `VmError` variants that +include formatted state snapshots where possible. + +--- + +## 4. Data-flow Walkthrough + +1. **Rule entry**: The compiler emits a `CallRule` instruction referencing a rule + index. The VM first consults `rule_cache[rule_index]`; non-function rules that + have already executed within the current top-level run reuse the cached + result. When the cache is cold, the VM pushes a new rule frame, allocates a + register window and jumps to the rule entry point. Function rules always run + afresh today—per-specialisation memoization is not yet implemented. +2. **Literal loads**: `Load` and `Load*` instructions fill registers from the + literal table or other sources (`LoadData`, `LoadInput`). +3. **Loops**: `LoopStart` fetches `LoopStartParams` from `InstructionData`, + initialises a `LoopContext`, and either pushes a new execution frame (for + suspendable mode) or updates `loop_stack`. `LoopNext` consults loop mode + (`Any`, `Every`, `ForEach`) to decide whether to continue or short-circuit. +4. **Comprehensions**: `ComprehensionBegin`/`Yield`/`End` manage collection + builders stored in a `ComprehensionContext`. Nested comprehensions stack + cleanly with loops. +5. **Assertions**: `AssertCondition` and `AssertNotUndefined` enforce Rego's + truthiness semantics. Inside loops/comprehensions they flag the current + iteration as failed (or short-circuit `every` loops to `false`); outside loop + contexts they raise `VmError::AssertionFailed`, mirroring Rego's runtime + errors for failed guards. +6. **Builtins & functions**: `BuiltinCall` reads `BuiltinCallParams`, resolves + the host function via `get_resolved_builtin`, and writes the result. Function + rules use `FunctionCallParams` to marshal arguments and run in the same + pipeline; repeat invocations with the same arguments are recomputed until the + VM grows specialisation-aware caching. +7. **Host await**: In run-to-completion mode, `HostAwait` consumes a response + from `host_await_responses`. Suspendable mode yields control with a + `SuspendReason::HostAwait { dest, argument, identifier }` that the host must + service. +8. **Completion**: `Return` wraps the selected register value into + `InstructionOutcome::Return`, unwinding frames until the entry frame is + cleared. `RuleReturn` is a specialised variant used by rule execution + helpers. + +Throughout execution, diagnostics (register snapshots, loop counters, cache +hits) can be collected via `RegoVM` accessors. Integration tests in +`tests/rvm/vm/suites` exercise the most complex combinations of loops, +comprehensions and host calls; `complex.yaml` is a good starting point for +understanding real-world instruction streams. + +--- + +## 5. Related Documentation + +- [Instruction Set Reference](instruction-set.md) +- [VM Runtime Walkthrough](vm-runtime.md) diff --git a/docs/rvm/instruction-set.md b/docs/rvm/instruction-set.md new file mode 100644 index 0000000..012de83 --- /dev/null +++ b/docs/rvm/instruction-set.md @@ -0,0 +1,238 @@ +# RVM Instruction Set Reference + +This reference captures every opcode emitted by the compiler and executed by +`RegoVM`. Each instruction is defined in `src/rvm/instructions/mod.rs` and +implemented by the dispatcher tree in `src/rvm/vm/dispatch.rs` plus specialised +submodules (`arithmetic.rs`, `loops.rs`, `functions.rs`, `rules.rs`, +`comprehension.rs`, `virtual_data.rs`). + +Use this guide to understand operand semantics, parameter tables, and runtime +side effects. + +--- + +## Reading the tables + +- **Operands**: registers (`rX`), literals (`litY`), parameter indices (`pZ`) and + immediate values. +- **Parameters**: links into `InstructionData` (`src/rvm/instructions/params.rs`). + The compiler stores complex metadata here; instructions reference it by index. +- **Outcome**: mentioned in prose where relevant (`Continue`, `Return`, `Break`, + `Suspend`). + +--- + +## Load and Move instructions + +| Mnemonic | Operands | Behaviour | +| :--------- | :-------------------------- | :--------------------------------------------------- | +| `Load` | `dest=rD, literal_idx=litN` | Copies literal `N` into register `D`. | +| `LoadTrue` | `dest=rD` | Stores boolean `true`. | +| `LoadFalse`| `dest=rD` | Stores boolean `false`. | +| `LoadNull` | `dest=rD` | Stores `Value::Null`. | +| `LoadBool` | `dest=rD, value` | Stores inline boolean literal. | +| `LoadData` | `dest=rD` | Stores the VM's `data` value. | +| `LoadInput`| `dest=rD` | Stores the VM's `input` value. | +| `Move` | `dest=rD, src=rS` | Copies register `S` into register `D`. | + +Out-of-range literal indices raise `VmError::LiteralIndexOutOfBounds`. Registers +must have been allocated by the current frame. + +--- + +## Arithmetic and comparison instructions + +| Mnemonic | Operands | Behaviour | +| :------- | :---------------------- | :------------------------------------------------------------ | +| `Add` | `dest, left, right` | Numeric addition; undefined operands trigger loop condition checks. | +| `Sub` | `dest, left, right` | Numeric subtraction. | +| `Mul` | `dest, left, right` | Numeric multiplication. | +| `Div` | `dest, left, right` | Numeric division with runtime checks (division by zero errors). | +| `Mod` | `dest, left, right` | Modulo. | +| `Eq` | `dest, left, right` | Equality comparison resulting in `Value::Bool`. | +| `Ne` | `dest, left, right` | Inequality. | +| `Lt`/`Le`/`Gt`/`Ge` | `dest, left, right` | Ordering comparisons. | +| `And` | `dest, left, right` | Logical conjunction (truthiness semantics). | +| `Or` | `dest, left, right` | Logical disjunction. | +| `Not` | `dest, operand` | Logical negation. | +| `AssertCondition` | `condition` | Fails current loop/rule when the condition is falsey. | +| `AssertNotUndefined` | `register` | Fails when register holds `Value::Undefined`. | + +`handle_condition` routes through `loops.rs` to propagate failures to loop and +comprehension contexts. Outside loops it aborts the current rule. + +--- + +## Collection and indexing instructions + +| Mnemonic | Operands / Params | Behaviour | +| :------------------------- | :---------------------------- | :---------------------------------------------------------- | +| `ObjectSet` | `obj, key, value` | Mutates object in `obj` with key/value from registers. | +| `ObjectCreate` | `params_index=pN` | Builds object from literal template and register entries. | +| `ArrayNew` | `dest` | Creates empty array. | +| `ArrayPush` | `arr, value` | Appends to array. | +| `ArrayCreate` | `params_index=pN` | Builds array from register list; undefined element ⇒ result undefined. | +| `SetNew` | `dest` | Creates empty set. | +| `SetAdd` | `set, value` | Adds element to set. | +| `SetCreate` | `params_index=pN` | Builds set from register list; undefined element ⇒ result undefined. | +| `Index` | `dest, container, key` | Indexes container with runtime key. | +| `IndexLiteral` | `dest, container, literal_idx`| Indexes container using literal stored in program. | +| `ChainedIndex` | `params_index=pN` | Resolves multi-hop path from root register. | +| `Contains` | `dest, collection, value` | Checks membership; returns `Value::Bool`. | +| `Count` | `dest, collection` | Returns length or `Value::Undefined` for unsupported types. | +| `VirtualDataDocumentLookup`| `params_index=pN` | Evaluates `data` path, invoking rules lazily. | + +Parameter structures: + +- `ObjectCreateParams` reuses arrays of literal key/value pairs and register + pairs. Literal keys must be sorted to match template order. +- `ArrayCreateParams` and `SetCreateParams` store register lists. The VM checks + all referenced registers for `Value::Undefined` before constructing the + collection. +- `VirtualDataDocumentLookupParams` and `ChainedIndexParams` encode `Vec` + path components. `LiteralOrRegister` is defined in `src/rvm/instructions/types.rs`. + +--- + +## Loop instructions + +Loops use dedicated parameter tables (`LoopStartParams`) and the `LoopMode` +enum. + +| Mnemonic | Operands / Params | Behaviour | +| :---------- | :----------------------- | :------------------------------------------------------------- | +| `LoopStart` | `params_index=pN` | Initialises loop context and decides first body iteration. | +| `LoopNext` | `body_start`, `loop_end` | Finalises iteration, updates accumulators, advances to next element. | + +`LoopMode` values: + +- `Any`: succeed on first passing iteration, short-circuit on success. +- `Every`: fail on first failing iteration. +- `ForEach`: evaluate all iterations, typically for comprehensions or complete + rules. + +`LoopStartParams` fields: + +- `collection`: source register. +- `key_reg` / `value_reg`: iteration registers (for arrays, key is index). +- `result_reg`: accumulator storing loop outcome (`bool` for quantifiers). +- `body_start` / `loop_end`: PCs identifying loop boundaries. + +The dispatcher converts `LoopStartParams` into a VM-specific `LoopParams` used by +both execution modes. In suspendable mode, loops own their own `ExecutionFrame`. + +--- + +## Comprehension instructions + +| Mnemonic | Operands / Params | Behaviour | +| :------------------- | :---------------------- | :------------------------------------------------- | +| `ComprehensionBegin` | `params_index=pN` | Allocates collection builder and iteration context. | +| `ComprehensionYield` | `value_reg`, `key_reg?` | Emits value (and optional key) into builder. | +| `ComprehensionEnd` | — | Finalises collection and stores result. | + +`ComprehensionBeginParams` captures: + +- `mode: ComprehensionMode` (Set, Array, Object) +- `collection_reg`: source register for iteration +- `result_reg`: register that will hold the final collection +- `key_reg` / `value_reg`: iteration registers +- `body_start` / `comprehension_end`: branch targets + +Comprehensions manage their own stack (`ComprehensionContext`) to maintain +ordering guarantees (arrays), uniqueness (sets) or key/value pairing (objects). + +--- + +## Call and return instructions + +| Mnemonic | Operands / Params | Behaviour | +| :-------------------- | :------------------------ | :---------------------------------------------- | +| `BuiltinCall` | `params_index=pN` | Invokes builtin via resolved function pointer. | +| `FunctionCall` | `params_index=pN` | Invokes function rule. | +| `CallRule` | `dest, rule_index` | Requests rule evaluation with caching. | +| `RuleInit` | `result_reg, rule_index` | Prepares rule accumulator and cache state. | +| `Return` | `value_reg` | Returns value from current function body. | +| `RuleReturn` | — | Finalises rule evaluation frame. | +| `DestructuringSuccess`| — | Signals successful destructuring, breaks rule block. | + +Parameter tables: + +- `BuiltinCallParams` / `FunctionCallParams` store destination register, index + into builtin table / rule index, argument count and up to eight argument + register numbers. +- The VM dynamically resizes registers when a callee requires a larger window + using program metadata (`max_rule_window_size`). + +--- + +## Host interaction + +| Mnemonic | Operands | Behaviour | +| :--------- | :---------------- | :--------------------------------------- | +| `HostAwait`| `dest, arg, id` | Yields control to host with payload value. | + +- Run-to-completion: consumes a response from `host_await_responses` keyed by + the identifier register. Missing responses raise `VmError::HostAwaitResponseMissing`. +- Suspendable: emits `InstructionOutcome::Suspend` with `SuspendReason::HostAwait`. + The host must resume with a value that will be written into `dest`. + +--- + +## Halt instruction + +| Mnemonic | Behaviour | Notes | +| :------- | :-------------------------------- | :---- | +| `Halt` | Terminates execution immediately. | Used during debugging or emitted for guard rails. | + +When encountered during run-to-completion execution, `Halt` returns the current +value in register `0`. + +--- + +## Parameter data overview + +`InstructionData` (`src/rvm/instructions/params.rs`) collects all complex +parameter types. Each `add_*` method returns a `u16` index suitable for storing +inside instructions. The VM retrieves tables via `get_*` accessors. + +| Struct | Field | Purpose | +| :----------------------- | :---------------------------------------- | :------------------------------------------------------------------------- | +| `LoopStartParams` | `mode` | Loop semantics (`Any`, `Every`, `ForEach`). | +| | `collection` | Register holding the iterable collection. | +| | `key_reg` / `value_reg` | Registers populated with the current key/value each iteration. | +| | `result_reg` | Accumulator for loop outcome (`bool` for quantifiers). | +| | `body_start` / `loop_end` | Instruction pointers delimiting the loop body and exit. | +| `BuiltinCallParams` | `dest` | Register that receives the builtin result. | +| | `builtin_index` | Slot into `builtin_info_table` for dispatch. | +| | `num_args` | Count of argument registers actually populated. | +| | `args[8]` | Up to eight registers supplying builtin arguments. | +| `FunctionCallParams` | `dest` | Register that receives the function rule result. | +| | `func_rule_index` | Rule index for the target function definition. | +| | `num_args` | Number of argument registers provided. | +| | `args[8]` | Argument register numbers (unused slots ignored). | +| `ObjectCreateParams` | `dest` | Destination register for the constructed object. | +| | `template_literal_idx` | Literal template containing all expected keys. | +| | `literal_key_fields: Vec<(u16, u8)>` | Mapping of literal-key indices to value registers. | +| | `fields: Vec<(u8, u8)>` | Dynamic key/value register pairs for non-literal keys. | +| `ArrayCreateParams` | `dest` | Destination register for the array literal. | +| | `elements: Vec` | Registers providing array elements (order preserved). | +| `SetCreateParams` | `dest` | Destination register for the set literal. | +| | `elements: Vec` | Registers providing set members (duplicates dropped at runtime). | +| `VirtualDataDocumentLookupParams` | `dest` | Destination register for lookup result. | +| | `path_components: Vec` | Ordered path traversal steps; mix of literals and register-based keys. | +| `ChainedIndexParams` | `dest` | Destination register for resolved value. | +| | `root` | Register containing the root object/collection. | +| | `path_components: Vec` | Path components applied relative to the root register. | +| `ComprehensionBeginParams` | `mode` | Comprehension output type (array, set, object). | +| | `collection_reg` | Source collection register for iteration. | +| | `result_reg` | Register receiving the final collection. | +| | `key_reg` / `value_reg` | Iteration registers (keys optional for arrays/sets). | +| | `body_start` / `comprehension_end` | Instruction pointers framing comprehension body and exit. | + +All parameter structs derive `Serialize`/`Deserialize` and can be stored inside +artifacts. Some contain `Vec` fields; the compiler is responsible for ensuring +indices remain valid and stable across serialization boundaries. + +--- + diff --git a/docs/rvm/vm-runtime.md b/docs/rvm/vm-runtime.md new file mode 100644 index 0000000..b7ec558 --- /dev/null +++ b/docs/rvm/vm-runtime.md @@ -0,0 +1,243 @@ +# VM Runtime Walkthrough + +This document explains the runtime architecture implemented under +`src/rvm/vm`. It focuses on the `RegoVM` struct, execution modes, and the +responsibilities of each support module. + +--- + +## 1. RegoVM structure + +`src/rvm/vm/machine.rs` defines the public entry point. The table below maps its +fields to responsibilities. + +| Field | Purpose | Related modules | +| :------------------------------------ | :---------------------------------------------------------- | :-------------- | +| `registers: Vec` | Active register window for the current frame. | `execution.rs`, `dispatch.rs` | +| `pc: usize` | Instruction pointer in run-to-completion mode. | `execution.rs` | +| `program: Arc` | Loaded program artifact. | `program/core.rs` | +| `compiled_policy` | Optional legacy default-rule support. | `crate::CompiledPolicy` | +| `rule_cache: Vec<(bool, Value)>` | Memoized rule results (bool = computed). | `rules.rs` | +| `data`, `input` | Global documents injected by host. | `dispatch.rs`, `virtual_data.rs` | +| `loop_stack` | Stack of `LoopContext` for run-to-completion loops. | `loops.rs` | +| `call_rule_stack` | Stack of `CallRuleContext` for nested rule calls. | `rules.rs` | +| `register_stack` | Saves prior register windows during run-to-completion rule calls. | `rules.rs`, `state.rs` | +| `comprehension_stack` | Active `ComprehensionContext` objects. | `comprehension.rs` | +| `base_register_count` | Root window size derived from program metadata. | `load_program` | +| `register_window_pool` | Recycled register vectors to reduce allocations. | `state.rs`, `rules.rs` | +| `max_instructions`, `executed_instructions` | Instruction budget and counter. | `execution.rs` | +| `evaluated` | Cache for virtual document lookups. | `virtual_data.rs` | +| `cache_hits` | Counters aiding diagnostics. | `virtual_data.rs` | +| `execution_stack` | Explicit frame stack for suspendable mode. | `execution_model.rs` | +| `execution_state` | `ExecutionState` enum capturing Ready/Running/Suspended/Error/Completed. | `execution_model.rs`, `execution.rs` | +| `breakpoints` | Set of PCs that trigger suspension. | `execution_model.rs` | +| `step_mode` | Enables single-step suspension after each instruction. | `execution.rs` | +| `host_await_responses` | Pre-scripted responses keyed by identifier (run-to-completion). | `dispatch.rs` | +| `execution_mode` | `RunToCompletion` or `Suspendable`. | `execution.rs` | +| `frame_pc_overridden` | Tracks manual PC updates inside frames. | `execution.rs`, `loops.rs`, `comprehension.rs` | +| `strict_builtin_errors` | Configures builtin failure handling (error vs `undefined`). | `machine.rs`, `arithmetic.rs`, `dispatch.rs` | + +### Key methods + +- `new` / `new_with_policy`: initialise VM with default register windows and + instruction limits. +- `load_program`: attaches a compiled `Program`, resizes registers, seeds rule + cache and resets counters. +- `set_data` / `set_input`: inject host documents. `set_data` runs + `Program::check_rule_data_conflicts` to guard against rule/data collisions. +- `set_max_instructions`, `set_execution_mode`, `set_step_mode`: configure + runtime policy. +- `set_host_await_responses`: used in run-to-completion mode when host await + responses are known ahead of time. +- `set_strict_builtin_errors`: toggles builtin failure semantics between + `VmError::ArithmeticError` and returning `Value::Undefined`. +- Accessors (`get_pc`, `get_registers`, `get_loop_stack`, etc.) aid debugging + and visualisation tooling. + +--- + +## 2. Execution modes + +### Run-to-completion + +- Entry path: `execute()` or `execute_entry_point_by_*` when + `ExecutionMode::RunToCompletion`. +- `execute_run_to_completion` resets state, marks `ExecutionState::Running` and + calls `jump_to(start_pc)`. +- `jump_to` loops over instructions, updating `pc` and calling + `execute_instruction`. The loop stops on `Return`, `Break`, or `VmError`. + `Break` (emitted by `RuleReturn` and `DestructuringSuccess`) returns + register 0 to the caller for compatibility with rule evaluation. +- Suspension is not allowed; encountering an instruction that would suspend + (e.g. `HostAwait`) raises an internal error. +- Instruction budgets trigger `VmError::InstructionLimitExceeded` and switch the + state to `ExecutionState::Error`. + +### Suspendable + +- Entry path: same public API, but the VM calls `run_stackless_from`. +- `run_stackless_from` pushes an initial `ExecutionFrame::main(start_pc, 0)` + onto `execution_stack` and dispatches instructions via `run_stackless_loop`. +- Each frame tracks its own `pc` and `FrameKind` (`Main`, `Rule`, `Loop`, + `Comprehension`). +- `RuleFrameData` carries scheduling cursors, register window sizing, and saved + copies of the caller's registers and stacks so finalisation can restore the + original context. +- Suspension: `InstructionOutcome::Suspend` records a `SuspendReason` (host + await, breakpoint, step) and stores the last result snapshot. Host code calls + `resume(resume_value)` to continue. +- Completion: when `execution_stack` becomes empty the VM sets + `ExecutionState::Completed { result }`. + +`execution_model.rs` defines the frame types and state machine: + +- `ExecutionFrame`: captures the frame-local `pc` together with its + `FrameKind` payload. +- `RuleFrameData`: tracks rule index, scheduling phase, register window sizing, + and the saved caller state (`saved_registers`, `saved_loop_stack`, + `saved_comprehension_stack`). +- `SuspendReason`: currently surfaced values are host await, breakpoint, and + step; additional variants (`SuspendInstruction`, `InstructionLimit`, + `External`) are reserved for future instructions. + +--- + +## 3. Instruction dispatch + +`dispatch.rs` routes each `Instruction` variant through layered helpers +(`execute_load_and_move`, `execute_arithmetic_instruction`, `execute_call_instruction`, etc.). Control +flow hinges on the `InstructionOutcome` enum: + +- `Continue`: normal execution; the caller increments the frame `pc`. +- `Return(Value)`: unwinds the current rule/function frame, propagating the + value upward. +- `Break`: used for rule-specific constructs (destructuring success, rule + return) to exit to the owning frame without returning a value. +- `Suspend { reason }`: used exclusively in suspendable mode. + +Arithmetic and comparison opcodes live in `arithmetic.rs`, honouring +`strict_builtin_errors` when operand types differ. Collection, loop, and +virtual-data operations share helpers that convert `Value` variants with runtime +type checking. Errors become `VmError` variants to ensure consistent reporting. +`Halt` returns the value stored in register 0, allowing bytecode to terminate +early without suspending. + +--- + +## 4. Loops and comprehensions + +`loops.rs` implements iteration. Major components: + +- `LoopContext`: stores iteration state (`IterationState` enum), key/value/result + registers, body and exit PCs, counters, and loop mode. +- `IterationState`: variants for arrays, objects, sets. Tracks progress for both + execution modes. +- `LoopMode`: `Any`, `Every`, `ForEach` controls short-circuit behaviour. + +`execute_loop_start` initialises iteration, pushing the context onto +`loop_stack` (run-to-completion) or embedding it into a `FrameKind::Loop` +(suspendable). `LoopParams` carries the bytecode offsets, registers, and +destinations required by the instruction. `LoopNext` evaluates the previous +iteration outcome, updates `success_count`, advances the iterator, overrides +the caller's PC when needed, and decides whether to continue or exit. + +`comprehension.rs` parallels `loops.rs` but maintains builder collections in +`ComprehensionContext`. The context stores: + +- Builder value (array, set, object). +- Pending key/value registers. +- `body_start` / `comprehension_end` PCs. +- `iteration_state` for nested loops bound to the comprehension. + +`ComprehensionYield` writes to the builder, respecting set uniqueness and object +key/value pairing. `ComprehensionEnd` publishes the result to `result_reg` and +pops the context. + +--- + +## 5. Rule execution and caching + +`rules.rs` and `functions.rs` coordinate rule calls: + +- `execute_call_rule` dispatches based on execution mode. Both paths consult + `rule_cache` and short-circuit if the result is already available. +- Run-to-completion (`execute_call_rule_common`): swaps the active register, + loop, and comprehension stacks; pushes them onto `register_stack`; and drives + bodies via `jump_to`. Successful results are cached for non-function rules. +- Suspendable (`execute_call_rule_suspendable`): builds a `RuleFrameData` + containing saved registers/stacks and pushes a `FrameKind::Rule` so the + stackless loop can schedule destructuring, bodies, and finalisation. +- `execute_rule_init` writes the rule result register to `Value::Undefined` + (or initialises sets/objects) before running the bodies and records the + result register in the active `CallRuleContext`. +- `execute_rule_return` lets the scheduler finalise the frame and propagate the + cached value to the caller. +- `functions.rs::execute_function_call` prepares argument registers and delegates + to `execute_call_rule*`, enforcing arity via `BuiltinInfo` metadata. + +Rule destructuring relies on `CallRuleContext`, +`RuleFramePhase::ExecutingDestructuring`, and the `DestructuringSuccess` +instruction to detect when pattern matching succeeded before entering the body. + +--- + +## 6. Virtual data lookups + +`virtual_data.rs` implements `VirtualDataDocumentLookup` and caches intermediate +path results in the VM's `evaluated` field (using `Value::Undefined` as a +sentinel) to avoid repeated rule evaluations. The compiler may set +`Program::needs_runtime_recursion_check`; the runtime currently relies on the +instruction budget and caching to prevent runaway recursion. Paths consist of +literals and register values supplied via `VirtualDataDocumentLookupParams`. + +`ChainedIndex` follows a similar pattern but operates on register roots instead +of the global `data` namespace. + +--- + +## 7. Error handling and diagnostics + +`errors.rs` defines the `VmError` enum. Common variants include: + +- `InstructionLimitExceeded` +- `LiteralIndexOutOfBounds` +- `RegisterNotArray` / `RegisterNotObject` +- `InvalidEntryPointIndex` / `EntryPointNotFound` +- `ArithmeticError` +- `RuleDataConflict` +- `HostAwaitResponseMissing` +- `Internal(String)` for invariant violations + +`execution.rs::handle_instruction_error` centralises error propagation. In +suspendable mode it unwinds frames while preserving partial results where +possible. Run-to-completion mode returns the error immediately. + +`state.rs` provides helpers to reset the VM and emit debug snapshots used in +assertion messages. When an internal invariant fails, the error message includes +`self.get_debug_state()` to help diagnose the issue. + +--- + +## 8. Operational guidance + +- **Instruction budgets**: adjust via `set_max_instructions` when running + untrusted policies. Inspect `executed_instructions` after completion. +- **Breakpoints & stepping**: populate `breakpoints` with bytecode PCs (see the + assembly listing) and enable `set_step_mode(true)` to pause after each + instruction. +- **Host await**: in run-to-completion mode, configure `set_host_await_responses` + before execution. In suspendable mode, expect `ExecutionState::Suspended { + reason: HostAwait { .. } }` and resume with the chosen value. +- **Builtin strictness**: `set_strict_builtin_errors(true)` reports type + mismatches as `VmError::ArithmeticError`; leave it `false` to coerce results + to `Value::Undefined`. +- **State inspection**: use getters (`get_registers`, `get_call_stack`, + `get_loop_stack`, `get_cache_hits`) to instrument evaluation or build + debugging UIs. `get_debug_state()` provides a concise snapshot for logs. +- **Testing**: YAML suites under `tests/rvm/vm/suites` exercise loops, + comprehensions, virtual data, host awaits, and serialization. `complex.yaml` + combines nested loops, comprehensions, function calls, and host awaits. + +--- + + diff --git a/scripts/pre-push b/scripts/pre-push index f49e6fe..3521f73 100755 --- a/scripts/pre-push +++ b/scripts/pre-push @@ -16,7 +16,7 @@ if [ -f Cargo.toml ]; then # Build for a target that has no std available. if command -v rustup > /dev/null; then rustup target add thumbv7m-none-eabi - (cd tests/ensure_no_std; cargo build -r --target thumbv7m-none-eabi) + (cd tests/ensure_no_std; cargo build -r --target thumbv7m-none-eabi --no-default-features --features opa-no-std) fi # Ensure that we can build with only std. diff --git a/src/lib.rs b/src/lib.rs index 269d1e1..b20190f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,8 @@ mod policy_info; mod query; #[cfg(feature = "azure_policy")] pub mod registry; +#[cfg(feature = "rvm")] +pub mod rvm; mod scheduler; #[cfg(feature = "azure_policy")] mod schema; diff --git a/src/rvm/instructions/display.rs b/src/rvm/instructions/display.rs new file mode 100644 index 0000000..e22de52 --- /dev/null +++ b/src/rvm/instructions/display.rs @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; + +use super::{Instruction, InstructionData, LiteralOrRegister}; + +impl Instruction { + /// Get detailed display string with parameter resolution for debugging + pub fn display_with_params(&self, instruction_data: &InstructionData) -> String { + match self { + Instruction::LoopStart { params_index } => { + if let Some(params) = instruction_data.get_loop_params(*params_index) { + format!( + "LOOP_START {:?} R({}) R({}) R({}) R({}) {} {}", + params.mode, + params.collection, + params.key_reg, + params.value_reg, + params.result_reg, + params.body_start, + params.loop_end + ) + } else { + format!("LOOP_START P({}) [INVALID INDEX]", params_index) + } + } + Instruction::BuiltinCall { params_index } => { + if let Some(params) = instruction_data.get_builtin_call_params(*params_index) { + let args_str = params + .arg_registers() + .iter() + .map(|&r| format!("R({})", r)) + .collect::>() + .join(" "); + format!( + "BUILTIN_CALL R({}) B({}) [{}]", + params.dest, params.builtin_index, args_str + ) + } else { + format!("BUILTIN_CALL P({}) [INVALID INDEX]", params_index) + } + } + Instruction::HostAwait { dest, arg, id } => { + format!("HOST_AWAIT R({}) R({}) R({})", dest, arg, id) + } + Instruction::FunctionCall { params_index } => { + if let Some(params) = instruction_data.get_function_call_params(*params_index) { + let args_str = params + .arg_registers() + .iter() + .map(|&r| format!("R({})", r)) + .collect::>() + .join(" "); + format!( + "FUNCTION_CALL R({}) RULE({}) [{}]", + params.dest, params.func_rule_index, args_str + ) + } else { + format!("FUNCTION_CALL P({}) [INVALID INDEX]", params_index) + } + } + Instruction::ObjectCreate { params_index } => { + if let Some(params) = instruction_data.get_object_create_params(*params_index) { + let mut field_parts = Vec::new(); + + // Add literal key fields + for &(literal_idx, value_reg) in params.literal_key_field_pairs() { + field_parts.push(format!("L({}):R({})", literal_idx, value_reg)); + } + + // Add non-literal key fields + for &(key_reg, value_reg) in params.field_pairs() { + field_parts.push(format!("R({}):R({})", key_reg, value_reg)); + } + + let fields_str = field_parts.join(" "); + format!( + "OBJECT_CREATE R({}) L({}) [{}]", + params.dest, params.template_literal_idx, fields_str + ) + } else { + format!("OBJECT_CREATE P({}) [INVALID INDEX]", params_index) + } + } + Instruction::VirtualDataDocumentLookup { params_index } => { + if let Some(params) = + instruction_data.get_virtual_data_document_lookup_params(*params_index) + { + let components_str = params + .path_components + .iter() + .map(|comp| match comp { + LiteralOrRegister::Literal(idx) => format!("L({})", idx), + LiteralOrRegister::Register(reg) => format!("R({})", reg), + }) + .collect::>() + .join("."); + format!( + "VIRTUAL_DATA_DOCUMENT_LOOKUP R({}) [data.{}]", + params.dest, components_str + ) + } else { + format!( + "VIRTUAL_DATA_DOCUMENT_LOOKUP P({}) [INVALID INDEX]", + params_index + ) + } + } + Instruction::ComprehensionBegin { params_index } => { + if let Some(params) = instruction_data.get_comprehension_begin_params(*params_index) + { + format!( + "COMPREHENSION_BEGIN {:?} R({}) R({}) R({}) {} {}", + params.mode, + params.collection_reg, + params.key_reg, + params.value_reg, + params.body_start, + params.comprehension_end + ) + } else { + format!("COMPREHENSION_BEGIN P({}) [INVALID INDEX]", params_index) + } + } + _ => self.to_string(), + } + } +} + +impl core::fmt::Display for Instruction { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let text = match self { + Instruction::Load { dest, literal_idx } => { + format!("LOAD R({}) L({})", dest, literal_idx) + } + Instruction::LoadTrue { dest } => format!("LOAD_TRUE R({})", dest), + Instruction::LoadFalse { dest } => format!("LOAD_FALSE R({})", dest), + Instruction::LoadNull { dest } => format!("LOAD_NULL R({})", dest), + Instruction::LoadBool { dest, value } => format!("LOAD_BOOL R({}) {}", dest, value), + Instruction::LoadData { dest } => format!("LOAD_DATA R({})", dest), + Instruction::LoadInput { dest } => format!("LOAD_INPUT R({})", dest), + Instruction::Move { dest, src } => format!("MOVE R({}) R({})", dest, src), + Instruction::Add { dest, left, right } => { + format!("ADD R({}) R({}) R({})", dest, left, right) + } + Instruction::Sub { dest, left, right } => { + format!("SUB R({}) R({}) R({})", dest, left, right) + } + Instruction::Mul { dest, left, right } => { + format!("MUL R({}) R({}) R({})", dest, left, right) + } + Instruction::Div { dest, left, right } => { + format!("DIV R({}) R({}) R({})", dest, left, right) + } + Instruction::Mod { dest, left, right } => { + format!("MOD R({}) R({}) R({})", dest, left, right) + } + Instruction::Eq { dest, left, right } => { + format!("EQ R({}) R({}) R({})", dest, left, right) + } + Instruction::Ne { dest, left, right } => { + format!("NE R({}) R({}) R({})", dest, left, right) + } + Instruction::Lt { dest, left, right } => { + format!("LT R({}) R({}) R({})", dest, left, right) + } + Instruction::Le { dest, left, right } => { + format!("LE R({}) R({}) R({})", dest, left, right) + } + Instruction::Gt { dest, left, right } => { + format!("GT R({}) R({}) R({})", dest, left, right) + } + Instruction::Ge { dest, left, right } => { + format!("GE R({}) R({}) R({})", dest, left, right) + } + Instruction::And { dest, left, right } => { + format!("AND R({}) R({}) R({})", dest, left, right) + } + Instruction::Or { dest, left, right } => { + format!("OR R({}) R({}) R({})", dest, left, right) + } + Instruction::Not { dest, operand } => { + format!("NOT R({}) R({})", dest, operand) + } + Instruction::BuiltinCall { params_index } => { + format!("BUILTIN_CALL P({})", params_index) + } + Instruction::HostAwait { dest, arg, id } => { + format!("HOST_AWAIT R({}) R({}) R({})", dest, arg, id) + } + Instruction::FunctionCall { params_index } => { + format!("FUNCTION_CALL P({})", params_index) + } + Instruction::Return { value } => format!("RETURN R({})", value), + Instruction::ObjectSet { obj, key, value } => { + format!("OBJECT_SET R({}) R({}) R({})", obj, key, value) + } + Instruction::ObjectCreate { params_index } => { + format!("OBJECT_CREATE P({})", params_index) + } + Instruction::Index { + dest, + container, + key, + } => format!("INDEX R({}) R({}) R({})", dest, container, key), + Instruction::IndexLiteral { + dest, + container, + literal_idx, + } => format!( + "INDEX_LITERAL R({}) R({}) L({})", + dest, container, literal_idx + ), + Instruction::ChainedIndex { params_index } => { + format!("CHAINED_INDEX P({})", params_index) + } + Instruction::ArrayNew { dest } => format!("ARRAY_NEW R({})", dest), + Instruction::ArrayPush { arr, value } => format!("ARRAY_PUSH R({}) R({})", arr, value), + Instruction::ArrayCreate { params_index } => { + format!("ARRAY_CREATE P({})", params_index) + } + Instruction::SetNew { dest } => format!("SET_NEW R({})", dest), + Instruction::SetAdd { set, value } => format!("SET_ADD R({}) R({})", set, value), + Instruction::SetCreate { params_index } => { + format!("SET_CREATE P({})", params_index) + } + Instruction::Contains { + dest, + collection, + value, + } => format!("CONTAINS R({}) R({}) R({})", dest, collection, value), + Instruction::Count { dest, collection } => { + format!("COUNT R({}) R({})", dest, collection) + } + Instruction::AssertCondition { condition } => { + format!("ASSERT_CONDITION R({})", condition) + } + Instruction::AssertNotUndefined { register } => { + format!("ASSERT_NOT_UNDEFINED R({})", register) + } + Instruction::LoopStart { params_index } => { + format!("LOOP_START P({})", params_index) + } + Instruction::LoopNext { + body_start, + loop_end, + } => { + format!("LOOP_NEXT {} {}", body_start, loop_end) + } + Instruction::CallRule { dest, rule_index } => { + format!("CALL_RULE R({}) {}", dest, rule_index) + } + Instruction::VirtualDataDocumentLookup { params_index } => { + format!("VIRTUAL_DATA_DOCUMENT_LOOKUP P({})", params_index) + } + Instruction::DestructuringSuccess {} => String::from("DESTRUCTURING_SUCCESS"), + Instruction::RuleReturn {} => String::from("RULE_RETURN"), + + Instruction::RuleInit { + result_reg, + rule_index, + } => { + format!("RULE_INIT R({}) {}", result_reg, rule_index) + } + Instruction::Halt {} => String::from("HALT"), + Instruction::ComprehensionBegin { params_index } => { + format!("COMPREHENSION_BEGIN P({})", params_index) + } + Instruction::ComprehensionYield { value_reg, key_reg } => match key_reg { + Some(k) => format!("COMPREHENSION_YIELD R({}) R({})", k, value_reg), + None => format!("COMPREHENSION_YIELD R({})", value_reg), + }, + Instruction::ComprehensionEnd {} => String::from("COMPREHENSION_END"), + }; + write!(f, "{}", text) + } +} diff --git a/src/rvm/instructions/mod.rs b/src/rvm/instructions/mod.rs new file mode 100644 index 0000000..792c80f --- /dev/null +++ b/src/rvm/instructions/mod.rs @@ -0,0 +1,381 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod display; +mod params; +mod types; + +pub use params::{ + ArrayCreateParams, BuiltinCallParams, ChainedIndexParams, ComprehensionBeginParams, + FunctionCallParams, InstructionData, LoopStartParams, ObjectCreateParams, SetCreateParams, + VirtualDataDocumentLookupParams, +}; +pub use types::{ComprehensionMode, LiteralOrRegister, LoopMode}; + +use serde::{Deserialize, Serialize}; + +/// RVM Instructions - simplified enum-based design +#[repr(C)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Instruction { + /// Load literal value from literal table into register + Load { + dest: u8, + literal_idx: u16, + }, + + /// Load true value into register + LoadTrue { + dest: u8, + }, + + /// Load false value into register + LoadFalse { + dest: u8, + }, + + /// Load null value into register + LoadNull { + dest: u8, + }, + + /// Load boolean value into register + LoadBool { + dest: u8, + value: bool, + }, + + /// Load global data object into register + LoadData { + dest: u8, + }, + + /// Load global input object into register + LoadInput { + dest: u8, + }, + + /// Move value from one register to another + Move { + dest: u8, + src: u8, + }, + + /// Arithmetic operations + Add { + dest: u8, + left: u8, + right: u8, + }, + Sub { + dest: u8, + left: u8, + right: u8, + }, + Mul { + dest: u8, + left: u8, + right: u8, + }, + Div { + dest: u8, + left: u8, + right: u8, + }, + Mod { + dest: u8, + left: u8, + right: u8, + }, + + /// Comparison operations + Eq { + dest: u8, + left: u8, + right: u8, + }, + Ne { + dest: u8, + left: u8, + right: u8, + }, + Lt { + dest: u8, + left: u8, + right: u8, + }, + Le { + dest: u8, + left: u8, + right: u8, + }, + Gt { + dest: u8, + left: u8, + right: u8, + }, + Ge { + dest: u8, + left: u8, + right: u8, + }, + + /// Logical operations + And { + dest: u8, + left: u8, + right: u8, + }, + Or { + dest: u8, + left: u8, + right: u8, + }, + Not { + dest: u8, + operand: u8, + }, + + /// Builtin function calls - optimized for builtin functions + BuiltinCall { + /// Index into program's instruction_data.builtin_call_params table + params_index: u16, + }, + + /// Suspend execution and yield control to the host + HostAwait { + /// Destination register to store the resume value + dest: u8, + /// Register containing the value to pass to the host + arg: u8, + /// Register containing a unique identifier for this await site + id: u8, + }, + + /// Function rule calls - for user-defined function rules + FunctionCall { + /// Index into program's instruction_data.function_call_params table + params_index: u16, + }, + + /// Return result + Return { + value: u8, + }, + + /// Set object field + ObjectSet { + obj: u8, + key: u8, + value: u8, + }, + + /// Create object with optimized field setting - uses parameter table + ObjectCreate { + /// Index into program's instruction_data.object_create_params table + params_index: u16, + }, + + /// Index into container (object, array, set) + Index { + dest: u8, + container: u8, + key: u8, + }, + + /// Index into container using literal key (optimization for Load + Index) + IndexLiteral { + dest: u8, + container: u8, + literal_idx: u16, + }, + + /// Multi-level chained indexing (e.g., obj.field1[expr].field2) + ChainedIndex { + /// Index into program's instruction_data.chained_index_params table + params_index: u16, + }, + + /// Create empty array + ArrayNew { + dest: u8, + }, + + /// Push element to array + ArrayPush { + arr: u8, + value: u8, + }, + + /// Create array from registers - returns undefined if any element is undefined + ArrayCreate { + /// Index into program's instruction_data.array_create_params table + params_index: u16, + }, + + /// Create empty set + SetNew { + dest: u8, + }, + + /// Add element to set + SetAdd { + set: u8, + value: u8, + }, + + /// Create set from registers - returns undefined if any element is undefined + SetCreate { + /// Index into program's instruction_data.set_create_params table + params_index: u16, + }, + + /// Check if collection contains value (for membership testing) + Contains { + dest: u8, + collection: u8, + value: u8, + }, + + /// Get count/length of collection (arrays, objects, sets) - returns undefined for non-collections + Count { + dest: u8, + collection: u8, + }, + + /// Assert condition - if register contains false or undefined, return undefined immediately + AssertCondition { + condition: u8, + }, + + /// Assert not undefined - if register contains undefined, return undefined immediately + AssertNotUndefined { + register: u8, + }, + + /// Start a loop over a collection with specified semantics - uses parameter table + LoopStart { + /// Index into program's instruction_data.loop_params table + params_index: u16, + }, + + /// Continue to next iteration or exit loop + LoopNext { + /// Jump target back to loop body + body_start: u16, + /// Jump target for loop end + loop_end: u16, + }, + + /// Call rule with caching - checks cache first, executes rule if needed, supports call stack + CallRule { + /// Destination register to store the result of the rule call + dest: u8, + /// Rule index to execute + rule_index: u16, + }, + + /// Initialize a rule + RuleInit { + /// The register where rule's result is accumulated. + result_reg: u8, + + /// The rule number of the rule + rule_index: u16, + }, + + /// Lookup in data namespace virtual documents (rules + base data) + VirtualDataDocumentLookup { + /// Index into program's instruction_data.virtual_data_document_lookup_params table + params_index: u16, + }, + + /// Mark successful completion of parameter destructuring validation + DestructuringSuccess {}, + + /// Return from rule execution + RuleReturn {}, + + /// Stop execution + Halt {}, + + /// Begin a comprehension with specified parameters + ComprehensionBegin { + /// Index into program's instruction_data.comprehension_begin_params table + params_index: u16, + }, + + /// Yield a value to the current comprehension result + ComprehensionYield { + /// Register containing the value to yield to the comprehension + value_reg: u8, + /// Optional register containing the key for object comprehensions + key_reg: Option, + }, + + /// End a comprehension block + ComprehensionEnd {}, +} + +impl Instruction { + /// Create a new LoopStart instruction with parameter table index + pub fn loop_start(params_index: u16) -> Self { + Self::LoopStart { params_index } + } + + /// Create a new BuiltinCall instruction with parameter table index + pub fn builtin_call(params_index: u16) -> Self { + Self::BuiltinCall { params_index } + } + + /// Create a new HostAwait instruction + pub fn host_await(dest: u8, arg: u8, id: u8) -> Self { + Self::HostAwait { dest, arg, id } + } + + /// Create a new FunctionCall instruction with parameter table index + pub fn function_call(params_index: u16) -> Self { + Self::FunctionCall { params_index } + } + + /// Create a new ObjectCreate instruction with parameter table index + pub fn object_create(params_index: u16) -> Self { + Self::ObjectCreate { params_index } + } + + /// Create a new ArrayCreate instruction with parameter table index + pub fn array_create(params_index: u16) -> Self { + Self::ArrayCreate { params_index } + } + + /// Create a new SetCreate instruction with parameter table index + pub fn set_create(params_index: u16) -> Self { + Self::SetCreate { params_index } + } + + /// Create a new ComprehensionBegin instruction with parameter table index + pub fn comprehension_begin(params_index: u16) -> Self { + Self::ComprehensionBegin { params_index } + } + + /// Create a new ComprehensionYield instruction + pub fn comprehension_yield(value_reg: u8) -> Self { + Self::ComprehensionYield { + value_reg, + key_reg: None, + } + } + + /// Create a new ComprehensionYield instruction for object comprehensions + pub fn comprehension_yield_object(key_reg: u8, value_reg: u8) -> Self { + Self::ComprehensionYield { + value_reg, + key_reg: Some(key_reg), + } + } + + /// Create a new ComprehensionEnd instruction + pub fn comprehension_end() -> Self { + Self::ComprehensionEnd {} + } +} diff --git a/src/rvm/instructions/params.rs b/src/rvm/instructions/params.rs new file mode 100644 index 0000000..216996b --- /dev/null +++ b/src/rvm/instructions/params.rs @@ -0,0 +1,445 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; + +use super::types::{ComprehensionMode, LiteralOrRegister, LoopMode}; + +/// Loop parameters stored in program's instruction data table +#[repr(C)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LoopStartParams { + /// Loop mode (Existential/Universal/Comprehension types) + pub mode: LoopMode, + /// Register containing the collection to iterate over + pub collection: u8, + /// Register to store current key (same as value_reg if key not needed) + pub key_reg: u8, + /// Register to store current value + pub value_reg: u8, + /// Register to store final result + pub result_reg: u8, + /// Jump target for loop body start + pub body_start: u16, + /// Jump target for loop end + pub loop_end: u16, +} + +/// Builtin function call parameters stored in program's instruction data table +#[repr(C)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BuiltinCallParams { + /// Destination register to store the result + pub dest: u8, + /// Index into program's builtin_info_table + pub builtin_index: u16, + /// Number of arguments actually used + pub num_args: u8, + /// Argument register numbers (unused slots contain undefined values) + pub args: [u8; 8], +} + +impl BuiltinCallParams { + /// Get the number of arguments actually used + pub fn arg_count(&self) -> usize { + self.num_args as usize + } + + /// Get argument register numbers as a slice + pub fn arg_registers(&self) -> &[u8] { + &self.args[..self.num_args as usize] + } +} + +/// Function rule call parameters stored in program's instruction data table +#[repr(C)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FunctionCallParams { + /// Destination register to store the result + pub dest: u8, + /// Rule index of the function to call + pub func_rule_index: u16, + /// Number of arguments actually used + pub num_args: u8, + /// Argument register numbers (unused slots contain undefined values) + pub args: [u8; 8], +} + +impl FunctionCallParams { + /// Get the number of arguments actually used + pub fn arg_count(&self) -> usize { + self.num_args as usize + } + + /// Get argument register numbers as a slice + pub fn arg_registers(&self) -> &[u8] { + &self.args[..self.num_args as usize] + } +} + +/// Object creation parameters stored in program's instruction data table +#[repr(C)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ObjectCreateParams { + /// Destination register to store the result object + pub dest: u8, + /// Literal index of template object with all keys (undefined values) + /// Always present - empty object if no literal keys + pub template_literal_idx: u16, + /// Fields with literal keys: (literal_key_index, value_register) in sorted order + pub literal_key_fields: Vec<(u16, u8)>, + /// Fields with non-literal keys: (key_register, value_register) + pub fields: Vec<(u8, u8)>, +} + +impl ObjectCreateParams { + /// Get the total number of fields + pub fn field_count(&self) -> usize { + self.literal_key_fields.len() + self.fields.len() + } + + /// Get literal key field pairs as a slice + pub fn literal_key_field_pairs(&self) -> &[(u16, u8)] { + &self.literal_key_fields + } + + /// Get non-literal key field pairs as a slice + pub fn field_pairs(&self) -> &[(u8, u8)] { + &self.fields + } +} + +/// Array creation parameters stored in program's instruction data table +#[repr(C)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ArrayCreateParams { + /// Destination register to store the result array + pub dest: u8, + /// Register numbers containing the element values + pub elements: Vec, +} + +impl ArrayCreateParams { + /// Get the number of elements + pub fn element_count(&self) -> usize { + self.elements.len() + } + + /// Get element register numbers as a slice + pub fn element_registers(&self) -> &[u8] { + &self.elements + } +} + +/// Set creation parameters stored in program's instruction data table +#[repr(C)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SetCreateParams { + /// Destination register to store the result set + pub dest: u8, + /// Register numbers containing the element values + pub elements: Vec, +} + +impl SetCreateParams { + /// Get the number of elements + pub fn element_count(&self) -> usize { + self.elements.len() + } + + /// Get element register numbers as a slice + pub fn element_registers(&self) -> &[u8] { + &self.elements + } +} + +/// Virtual data document lookup parameters for data namespace access with rule evaluation +#[repr(C)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VirtualDataDocumentLookupParams { + /// Destination register to store the result + pub dest: u8, + /// Path components in order (e.g., for data.users[input.name].config) + /// This would be [Literal("users"), Register(5), Literal("config")] + /// where register 5 contains the value from input.name + pub path_components: Vec, +} + +impl VirtualDataDocumentLookupParams { + /// Get the number of path components + pub fn component_count(&self) -> usize { + self.path_components.len() + } + + /// Check if all components are literals (can be optimized at compile time) + pub fn all_literals(&self) -> bool { + self.path_components + .iter() + .all(|c| matches!(c, LiteralOrRegister::Literal(_))) + } + + /// Get just the literal indices (for debugging/display) + pub fn literal_indices(&self) -> Vec { + self.path_components + .iter() + .filter_map(|c| match c { + LiteralOrRegister::Literal(idx) => Some(*idx), + _ => None, + }) + .collect() + } + + /// Get just the register numbers (for debugging/display) + pub fn register_numbers(&self) -> Vec { + self.path_components + .iter() + .filter_map(|c| match c { + LiteralOrRegister::Register(reg) => Some(*reg), + _ => None, + }) + .collect() + } +} + +/// Chained index parameters for multi-level object access (input, locals, non-rule data paths) +#[repr(C)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChainedIndexParams { + /// Destination register to store the result + pub dest: u8, + /// Root register containing the base object (input, local var, data subset) + pub root: u8, + /// Path components to traverse from the root + pub path_components: Vec, +} + +impl ChainedIndexParams { + /// Get the number of path components + pub fn component_count(&self) -> usize { + self.path_components.len() + } + + /// Check if all components are literals (can be optimized) + pub fn all_literals(&self) -> bool { + self.path_components + .iter() + .all(|c| matches!(c, LiteralOrRegister::Literal(_))) + } + + /// Get just the literal indices (for debugging/display) + pub fn literal_indices(&self) -> Vec { + self.path_components + .iter() + .filter_map(|c| match c { + LiteralOrRegister::Literal(idx) => Some(*idx), + _ => None, + }) + .collect() + } + + /// Get just the register numbers (for debugging/display) + pub fn register_numbers(&self) -> Vec { + self.path_components + .iter() + .filter_map(|c| match c { + LiteralOrRegister::Register(reg) => Some(*reg), + _ => None, + }) + .collect() + } +} + +/// Comprehension parameters stored in program's instruction data table +#[repr(C)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComprehensionBeginParams { + /// Type of comprehension being created + pub mode: ComprehensionMode, + /// Register containing the source collection to iterate over + pub collection_reg: u8, + /// Register to store the comprehension result collection + /// If not specified separately, this will match collection_reg + pub result_reg: u8, + /// Register to store current iteration key + pub key_reg: u8, + /// Register to store current iteration value + pub value_reg: u8, + /// Jump target for comprehension body start + pub body_start: u16, + /// Jump target for comprehension end + pub comprehension_end: u16, +} + +/// Instruction data container for complex instruction parameters +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InstructionData { + /// Loop parameter table for LoopStart instructions + pub loop_params: Vec, + /// Builtin function call parameter table for BuiltinCall instructions + pub builtin_call_params: Vec, + /// Function rule call parameter table for FunctionCall instructions + pub function_call_params: Vec, + /// Object creation parameter table for ObjectCreate instructions + pub object_create_params: Vec, + /// Array creation parameter table for ArrayCreate instructions + pub array_create_params: Vec, + /// Set creation parameter table for SetCreate instructions + pub set_create_params: Vec, + /// Virtual data document lookup parameter table for VirtualDataDocumentLookup instructions + pub virtual_data_document_lookup_params: Vec, + /// Chained index parameter table for ChainedIndex instructions + pub chained_index_params: Vec, + /// Comprehension parameter table for ComprehensionBegin instructions + pub comprehension_begin_params: Vec, +} + +impl InstructionData { + /// Create a new empty instruction data container + pub fn new() -> Self { + Self { + loop_params: Vec::new(), + builtin_call_params: Vec::new(), + function_call_params: Vec::new(), + object_create_params: Vec::new(), + array_create_params: Vec::new(), + set_create_params: Vec::new(), + virtual_data_document_lookup_params: Vec::new(), + chained_index_params: Vec::new(), + comprehension_begin_params: Vec::new(), + } + } + + /// Add loop parameters and return the index + pub fn add_loop_params(&mut self, params: LoopStartParams) -> u16 { + let index = self.loop_params.len(); + self.loop_params.push(params); + index as u16 + } + + /// Add builtin call parameters and return the index + pub fn add_builtin_call_params(&mut self, params: BuiltinCallParams) -> u16 { + let index = self.builtin_call_params.len(); + self.builtin_call_params.push(params); + index as u16 + } + + /// Add function call parameters and return the index + pub fn add_function_call_params(&mut self, params: FunctionCallParams) -> u16 { + let index = self.function_call_params.len(); + self.function_call_params.push(params); + index as u16 + } + + /// Add object create parameters and return the index + pub fn add_object_create_params(&mut self, params: ObjectCreateParams) -> u16 { + let index = self.object_create_params.len(); + self.object_create_params.push(params); + index as u16 + } + + /// Add array create parameters and return the index + pub fn add_array_create_params(&mut self, params: ArrayCreateParams) -> u16 { + let index = self.array_create_params.len(); + self.array_create_params.push(params); + index as u16 + } + + /// Add set create parameters and return the index + pub fn add_set_create_params(&mut self, params: SetCreateParams) -> u16 { + let index = self.set_create_params.len(); + self.set_create_params.push(params); + index as u16 + } + + /// Get loop parameters by index + pub fn get_loop_params(&self, index: u16) -> Option<&LoopStartParams> { + self.loop_params.get(index as usize) + } + + /// Get builtin call parameters by index + pub fn get_builtin_call_params(&self, index: u16) -> Option<&BuiltinCallParams> { + self.builtin_call_params.get(index as usize) + } + + /// Get function call parameters by index + pub fn get_function_call_params(&self, index: u16) -> Option<&FunctionCallParams> { + self.function_call_params.get(index as usize) + } + + /// Get object create parameters by index + pub fn get_object_create_params(&self, index: u16) -> Option<&ObjectCreateParams> { + self.object_create_params.get(index as usize) + } + + /// Get array create parameters by index + pub fn get_array_create_params(&self, index: u16) -> Option<&ArrayCreateParams> { + self.array_create_params.get(index as usize) + } + + /// Get set create parameters by index + pub fn get_set_create_params(&self, index: u16) -> Option<&SetCreateParams> { + self.set_create_params.get(index as usize) + } + + /// Add virtual data document lookup parameters and return the index + pub fn add_virtual_data_document_lookup_params( + &mut self, + params: VirtualDataDocumentLookupParams, + ) -> u16 { + let index = self.virtual_data_document_lookup_params.len(); + self.virtual_data_document_lookup_params.push(params); + index as u16 + } + + /// Get virtual data document lookup parameters by index + pub fn get_virtual_data_document_lookup_params( + &self, + index: u16, + ) -> Option<&VirtualDataDocumentLookupParams> { + self.virtual_data_document_lookup_params.get(index as usize) + } + + /// Add chained index parameters and return the index + pub fn add_chained_index_params(&mut self, params: ChainedIndexParams) -> u16 { + let index = self.chained_index_params.len(); + self.chained_index_params.push(params); + index as u16 + } + + /// Get chained index parameters by index + pub fn get_chained_index_params(&self, index: u16) -> Option<&ChainedIndexParams> { + self.chained_index_params.get(index as usize) + } + + /// Get mutable reference to loop parameters by index + pub fn get_loop_params_mut(&mut self, index: u16) -> Option<&mut LoopStartParams> { + self.loop_params.get_mut(index as usize) + } + + /// Add comprehension begin parameters and return the index + pub fn add_comprehension_begin_params(&mut self, params: ComprehensionBeginParams) -> u16 { + let index = self.comprehension_begin_params.len(); + self.comprehension_begin_params.push(params); + index as u16 + } + + /// Get comprehension begin parameters by index + pub fn get_comprehension_begin_params(&self, index: u16) -> Option<&ComprehensionBeginParams> { + self.comprehension_begin_params.get(index as usize) + } + + /// Get mutable reference to comprehension begin parameters by index + pub fn get_comprehension_begin_params_mut( + &mut self, + index: u16, + ) -> Option<&mut ComprehensionBeginParams> { + self.comprehension_begin_params.get_mut(index as usize) + } +} + +impl Default for InstructionData { + fn default() -> Self { + Self::new() + } +} diff --git a/src/rvm/instructions/types.rs b/src/rvm/instructions/types.rs new file mode 100644 index 0000000..c19c243 --- /dev/null +++ b/src/rvm/instructions/types.rs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use serde::{Deserialize, Serialize}; + +/// Represents either a literal index or a register number for path components +#[repr(C)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum LiteralOrRegister { + /// Index into the program's literal table + Literal(u16), + /// Register number containing the value + Register(u8), +} + +/// Loop execution modes for different Rego iteration constructs +#[repr(C)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum LoopMode { + /// Any quantification: some x in arr, x := arr[_], etc. + /// Succeeds if ANY iteration succeeds, exits early on first success + Any, + + /// Every quantification: every x in arr + /// Succeeds only if ALL iterations succeed, exits early on first failure + Every, + + /// ForEach processing: processes all elements without early exit + /// Used for set membership rules (contains), object rules, and complete rules + /// where all candidates must be evaluated. Determined by output constness. + ForEach, +} + +/// Comprehension execution modes for different comprehension types +#[repr(C)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ComprehensionMode { + /// Set comprehension: {expr | condition} + /// Collects unique successful iterations into a set + Set, + /// Array comprehension: [expr | condition] + /// Collects successful iterations into an array (preserves order) + Array, + /// Object comprehension: {key: value | condition} + /// Collects successful key-value pairs into an object + Object, +} diff --git a/src/rvm/mod.rs b/src/rvm/mod.rs new file mode 100644 index 0000000..b2e2d63 --- /dev/null +++ b/src/rvm/mod.rs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// RVM - Rego Virtual Machine +// A register-based virtual machine for executing Rego policies + +pub mod instructions; +pub mod program; +pub mod tests; +pub mod vm; + +pub use instructions::Instruction; +pub use program::{ + generate_assembly_listing, generate_tabular_assembly_listing, AssemblyListingConfig, Program, +}; +pub use vm::RegoVM; diff --git a/src/rvm/program/core.rs b/src/rvm/program/core.rs new file mode 100644 index 0000000..80544c3 --- /dev/null +++ b/src/rvm/program/core.rs @@ -0,0 +1,322 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use anyhow::Result as AnyResult; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +use super::types::{BuiltinInfo, ProgramMetadata, RuleInfo, SourceFile, SpanInfo}; +use crate::builtins::BuiltinFcn; +use crate::rvm::instructions::InstructionData; +use crate::rvm::Instruction; +use crate::value::Value; + +/// Complete compiled program containing all execution artifacts +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Program { + /// Compiled bytecode instructions + pub instructions: Vec, + + /// Literal value table (skipped in serde, serialized separately as JSON) + #[serde(skip, default = "Vec::new")] + pub literals: Vec, + + /// Complex instruction parameter data (for LoopStart, Call, etc.) + pub instruction_data: InstructionData, + + /// Builtin function information table + pub builtin_info_table: Vec, + + /// Entry points mapping with preserved insertion order (skipped in serde, serialized separately as JSON) + #[serde(skip, default = "IndexMap::new")] + pub entry_points: IndexMap, + + /// Source files table with content (skipped in serde, serialized separately as JSON) + #[serde(skip, default = "Vec::new")] + pub sources: Vec, + + /// Rule metadata: rule_index -> rule information + pub rule_infos: Vec, + + /// Span information for each instruction (for debugging) + pub instruction_spans: Vec>, + + /// Main program entry point + pub main_entry_point: usize, + + /// Maximum register window size observed across all rule definitions + pub max_rule_window_size: usize, + + /// Register window size needed for entry point dispatch + pub dispatch_window_size: usize, + + /// Program metadata + pub metadata: ProgramMetadata, + + /// Rule tree for efficient rule lookup (skipped in serde, serialized separately as JSON) + /// Maps rule paths (e.g., "data.p1.r1") to rule indices + /// Structure: {"p1": {"r1": rule_index}, "p2": {"p3": {"r2": rule_index}}} + #[serde(skip, default = "Value::new_object")] + pub rule_tree: Value, + + /// Resolved builtins - actual builtin function values fetched from interpreter's builtin map + /// This field is skipped during serialization and reinitialized after deserialization + #[serde(skip)] + pub resolved_builtins: Vec, + + /// Flag indicating that VirtualDataDocumentLookup instruction was used and runtime recursion checking is needed + pub needs_runtime_recursion_check: bool, + + /// Flag indicating that recompilation is needed due to partial deserialization failure + /// This is set to true when the artifact section was successfully read but the extensible + /// section failed to deserialize (e.g., due to version incompatibility) + #[serde(default)] + pub needs_recompilation: bool, + + /// Rego language version used for compilation (true for Rego v0, false for Rego v1) + /// This must be preserved during recompilation to maintain policy semantics + /// Serialized separately in the artifact section for guaranteed availability + #[serde(skip, default)] + pub rego_v0: bool, +} + +impl Program { + /// Current serialization format version + pub const SERIALIZATION_VERSION: u32 = 3; + /// Magic bytes to identify Regorus program files + pub const MAGIC: [u8; 4] = *b"REGO"; + + /// Create a new empty program + pub fn new() -> Self { + Self { + instructions: Vec::new(), + literals: Vec::new(), + instruction_data: InstructionData::new(), + builtin_info_table: Vec::new(), + entry_points: IndexMap::new(), + sources: Vec::new(), + rule_infos: Vec::new(), + instruction_spans: Vec::new(), + main_entry_point: 0, + max_rule_window_size: 0, + dispatch_window_size: 0, + metadata: ProgramMetadata { + compiler_version: env!("CARGO_PKG_VERSION").to_string(), + compiled_at: "unknown".to_string(), + source_info: "unknown".to_string(), + optimization_level: 0, + }, + rule_tree: Value::new_object(), + resolved_builtins: Vec::new(), + needs_runtime_recursion_check: false, + needs_recompilation: false, + rego_v0: false, // Default to Rego v1 + } + } + + /// Add a source file and return its index + pub fn add_source(&mut self, name: String, content: String) -> usize { + let source_file = SourceFile::new(name.clone(), content); + let index = self.sources.len(); + self.sources.push(source_file); + index + } + + /// Add loop parameters and return the index + pub fn add_loop_params(&mut self, params: crate::rvm::instructions::LoopStartParams) -> u16 { + self.instruction_data.add_loop_params(params) + } + + /// Add comprehension begin parameters and return the index + pub fn add_comprehension_begin_params( + &mut self, + params: crate::rvm::instructions::ComprehensionBeginParams, + ) -> u16 { + self.instruction_data.add_comprehension_begin_params(params) + } + + /// Add builtin call parameters and return the index + pub fn add_builtin_call_params( + &mut self, + params: crate::rvm::instructions::BuiltinCallParams, + ) -> u16 { + self.instruction_data.add_builtin_call_params(params) + } + + /// Add function call parameters and return the index + pub fn add_function_call_params( + &mut self, + params: crate::rvm::instructions::FunctionCallParams, + ) -> u16 { + self.instruction_data.add_function_call_params(params) + } + + /// Add builtin info and return the index + pub fn add_builtin_info(&mut self, builtin_info: BuiltinInfo) -> u16 { + let index = self.builtin_info_table.len(); + self.builtin_info_table.push(builtin_info); + index as u16 + } + + /// Get builtin info by index + pub fn get_builtin_info(&self, index: u16) -> Option<&BuiltinInfo> { + self.builtin_info_table.get(index as usize) + } + + /// Update loop parameters by index + pub fn update_loop_params(&mut self, params_index: u16, updater: F) + where + F: FnOnce(&mut crate::rvm::instructions::LoopStartParams), + { + if let Some(params) = self.instruction_data.get_loop_params_mut(params_index) { + updater(params); + } + } + + /// Update comprehension begin parameters by index + pub fn update_comprehension_begin_params(&mut self, params_index: u16, updater: F) + where + F: FnOnce(&mut crate::rvm::instructions::ComprehensionBeginParams), + { + if let Some(params) = self + .instruction_data + .get_comprehension_begin_params_mut(params_index) + { + updater(params); + } + } + + /// Get detailed instruction display with parameter resolution + pub fn display_instruction_with_params(&self, instruction: &Instruction) -> String { + instruction.display_with_params(&self.instruction_data) + } + + /// Add a source file directly and return its index + pub fn add_source_file(&mut self, source_file: SourceFile) -> usize { + for (i, existing) in self.sources.iter().enumerate() { + if existing.name == source_file.name { + return i; + } + } + + let index = self.sources.len(); + self.sources.push(source_file); + index + } + + /// Get source file by index + pub fn get_source_file(&self, index: usize) -> Option<&SourceFile> { + self.sources.get(index) + } + + /// Get source content by index + pub fn get_source(&self, index: usize) -> Option<&str> { + self.sources.get(index).map(|s| s.content.as_str()) + } + + /// Get source name by index + pub fn get_source_name(&self, index: usize) -> Option<&str> { + self.sources.get(index).map(|s| s.name.as_str()) + } + + /// Get rule info by index + pub fn get_rule_info(&self, rule_index: usize) -> Option<&RuleInfo> { + self.rule_infos.get(rule_index) + } + + /// Get span information for instruction + pub fn get_instruction_span(&self, instruction_index: usize) -> Option<&SpanInfo> { + self.instruction_spans + .get(instruction_index) + .and_then(|span| span.as_ref()) + } + + /// Add instruction with optional span + pub fn add_instruction(&mut self, instruction: Instruction, span: Option) { + self.instructions.push(instruction); + self.instruction_spans.push(span); + } + + /// Add literal value and return its index + pub fn add_literal(&mut self, value: Value) -> usize { + for (i, existing) in self.literals.iter().enumerate() { + if existing == &value { + return i; + } + } + + let index = self.literals.len(); + self.literals.push(value); + index + } + + /// Initialize resolved builtins directly from the BUILTINS HashMap + /// This should be called after deserialization to populate the skipped field + /// Returns an error if any required builtin is missing + pub fn initialize_resolved_builtins(&mut self) -> AnyResult<()> { + self.resolved_builtins.clear(); + self.resolved_builtins + .reserve(self.builtin_info_table.len()); + + for builtin_info in &self.builtin_info_table { + if let Some(&builtin_fcn) = crate::builtins::BUILTINS.get(builtin_info.name.as_str()) { + self.resolved_builtins.push(builtin_fcn); + } else { + return Err(anyhow::anyhow!( + "Missing builtin function: {}", + builtin_info.name + )); + } + } + + Ok(()) + } + + /// Get resolved builtin function by index + pub fn get_resolved_builtin(&self, index: u16) -> Option<&BuiltinFcn> { + self.resolved_builtins.get(index as usize) + } + + /// Check if resolved builtins are initialized + pub fn has_resolved_builtins(&self) -> bool { + !self.resolved_builtins.is_empty() + } + + /// Add an entry point mapping from path to rule index + pub fn add_entry_point(&mut self, path: String, rule_index: usize) { + self.entry_points.insert(path, rule_index); + } + + /// Get rule index for an entry point path + pub fn get_entry_point(&self, path: &str) -> Option { + self.entry_points.get(path).copied() + } + + /// Get all entry points as IndexMap + pub fn get_entry_points(&self) -> &IndexMap { + &self.entry_points + } + + /// Check if recompilation is needed due to partial deserialization failure + pub fn needs_recompilation(&self) -> bool { + self.needs_recompilation + } + + /// Mark that recompilation is needed + pub fn set_needs_recompilation(&mut self, needs_recompilation: bool) { + self.needs_recompilation = needs_recompilation; + } + + /// Check if the program is fully functional (not needing recompilation) + pub fn is_fully_functional(&self) -> bool { + !self.needs_recompilation + } +} + +impl Default for Program { + fn default() -> Self { + Self::new() + } +} diff --git a/src/rvm/program/listing.rs b/src/rvm/program/listing.rs new file mode 100644 index 0000000..e48c73e --- /dev/null +++ b/src/rvm/program/listing.rs @@ -0,0 +1,964 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::fmt::Write; + +use crate::rvm::{ + instructions::{Instruction, InstructionData, LoopMode}, + program::Program, +}; + +/// Configuration for assembly listing output +#[derive(Debug, Clone)] +pub struct AssemblyListingConfig { + /// Show instruction addresses + pub show_addresses: bool, + /// Show raw instruction bytes (if available) + pub show_bytes: bool, + /// Indent size for nested loops + pub indent_size: usize, + /// Maximum width for instruction column + pub instruction_width: usize, + /// Show literal values inline + pub show_literal_values: bool, + /// Column position for comments + pub comment_column: usize, +} + +impl Default for AssemblyListingConfig { + fn default() -> Self { + Self { + show_addresses: true, + show_bytes: false, + indent_size: 4, + instruction_width: 40, + show_literal_values: true, + comment_column: 50, + } + } +} + +/// Generate annotated assembly listing for a compiled program +pub fn generate_assembly_listing(program: &Program, config: &AssemblyListingConfig) -> String { + let mut output = String::new(); + let mut indent_level: usize = 0; + let mut current_rule_index: Option = None; + + // Track active loops and comprehensions by their end addresses + let mut active_ends: Vec = Vec::new(); + + // Add header + writeln!( + output, + "; RVM Assembly - {} instructions, {} literals, {} builtins", + program.instructions.len(), + program.literals.len(), + program.builtin_info_table.len() + ) + .unwrap(); + + // Add builtins table + if !program.builtin_info_table.is_empty() { + writeln!(output, ";").unwrap(); + writeln!(output, "; BUILTINS TABLE:").unwrap(); + for (idx, builtin_info) in program.builtin_info_table.iter().enumerate() { + writeln!(output, "; B{:2}: {}", idx, builtin_info.name).unwrap(); + } + } + + // Add literals table + if config.show_literal_values && !program.literals.is_empty() { + writeln!(output, ";").unwrap(); + writeln!(output, "; LITERALS (JSON values):").unwrap(); + for (idx, literal) in program.literals.iter().enumerate() { + let literal_json = + serde_json::to_string(literal).unwrap_or_else(|_| "".to_string()); + writeln!(output, "; L{:2}: {}", idx, literal_json).unwrap(); + } + } + + // Add rules table if available + if !program.rule_infos.is_empty() { + writeln!(output, ";").unwrap(); + writeln!(output, "; RULES TABLE:").unwrap(); + for (idx, rule_info) in program.rule_infos.iter().enumerate() { + writeln!(output, "; R{:2}: {}", idx, rule_info.name).unwrap(); + } + } + + writeln!(output, ";").unwrap(); + + for (pc, instruction) in program.instructions.iter().enumerate() { + // Handle rule transitions and add gaps + if let Instruction::RuleInit { rule_index, .. } = instruction { + // Add gap before new rule (except for the first rule) + if current_rule_index.is_some() { + writeln!(output).unwrap(); + } + current_rule_index = Some(*rule_index); + + // Add rule name prefix + if let Some(rule_info) = program.rule_infos.get(*rule_index as usize) { + writeln!(output, "; ===== RULE: {} =====", rule_info.name).unwrap(); + } else { + writeln!(output, "; ===== RULE: rule_{} =====", rule_index).unwrap(); + } + } + + // Check if current PC matches any active end addresses (loops, comprehensions, rules) + let current_pc = pc as u16; + while let Some(&end_addr) = active_ends.last() { + if current_pc >= end_addr { + active_ends.pop(); + indent_level = indent_level.saturating_sub(1); + } else { + break; + } + } + + // Handle explicit end instructions + match instruction { + Instruction::LoopNext { .. } => { + // LoopNext already handled by end address tracking above + } + Instruction::RuleReturn { .. } => { + indent_level = indent_level.saturating_sub(1); + } + _ => {} + } + + // Special case: Block end instructions should be indented at their block level (one level out) + let effective_indent_level = match instruction { + Instruction::ComprehensionEnd {} => indent_level.saturating_sub(1), + Instruction::LoopNext { .. } => indent_level.saturating_sub(1), + _ => indent_level, + }; + + let indent = " ".repeat(effective_indent_level * config.indent_size); + + // Format address + let addr_str = if config.show_addresses { + format!("{:03}: ", pc) + } else { + String::new() + }; + + // Format instruction with proper indentation and aligned comments + let inst_str = format_instruction_readable( + instruction, + &indent, + &program.instruction_data, + program, + config, + ); + + writeln!(output, "{}{}", addr_str, inst_str).unwrap(); + + // Increase indentation for loop/rule/comprehension starts and track their end addresses + match instruction { + Instruction::LoopStart { params_index } => { + if let Some(params) = program.instruction_data.get_loop_params(*params_index) { + active_ends.push(params.loop_end); + indent_level += 1; + } + } + Instruction::ComprehensionBegin { params_index } => { + if let Some(params) = program + .instruction_data + .get_comprehension_begin_params(*params_index) + { + active_ends.push(params.comprehension_end); + indent_level += 1; + } + } + Instruction::RuleInit { .. } => { + indent_level += 1; + // Note: Rules end with RuleReturn, not an address, so we don't track them here + } + _ => {} + } + } + + output +} + +/// Helper function to align comments at a specific column +fn align_comment(base_text: &str, comment: &str, target_column: usize) -> String { + let current_len = base_text.len(); + if current_len >= target_column { + format!("{} ; {}", base_text, comment) + } else { + let padding = " ".repeat(target_column - current_len); + format!("{}{} ; {}", base_text, padding, comment) + } +} + +/// Format a single instruction with proper indentation and mathematical notation +fn format_instruction_readable( + instruction: &Instruction, + indent: &str, + instruction_data: &InstructionData, + program: &Program, + config: &AssemblyListingConfig, +) -> String { + match instruction { + Instruction::Load { dest, literal_idx } => { + let base = format!("{}Load r{} ← L{}", indent, dest, literal_idx); + let comment = if *literal_idx < program.literals.len() as u16 { + let literal_json = serde_json::to_string(&program.literals[*literal_idx as usize]) + .unwrap_or_else(|_| "".to_string()); + format!("Load literal: {}", literal_json) + } else { + "Load literal: ".to_string() + }; + align_comment(&base, &comment, config.comment_column) + } + Instruction::LoadTrue { dest } => { + let base = format!("{}LoadTrue r{} ← true", indent, dest); + align_comment(&base, "Load boolean constant true", config.comment_column) + } + Instruction::LoadFalse { dest } => { + let base = format!("{}LoadFalse r{} ← false", indent, dest); + align_comment(&base, "Load boolean constant false", config.comment_column) + } + Instruction::LoadNull { dest } => { + let base = format!("{}LoadNull r{} ← null", indent, dest); + align_comment(&base, "Load null value", config.comment_column) + } + Instruction::LoadBool { dest, value } => { + let base = format!("{}LoadBool r{} ← {}", indent, dest, value); + let comment = format!("Load boolean constant {}", value); + align_comment(&base, &comment, config.comment_column) + } + Instruction::LoadData { dest } => { + let base = format!("{}LoadData r{} ← data", indent, dest); + align_comment(&base, "Load global data document", config.comment_column) + } + Instruction::LoadInput { dest } => { + let base = format!("{}LoadInput r{} ← input", indent, dest); + align_comment(&base, "Load global input document", config.comment_column) + } + Instruction::Move { dest, src } => { + let base = format!("{}Move r{} ← r{}", indent, dest, src); + let comment = format!("Copy value from r{} to r{}", src, dest); + align_comment(&base, &comment, config.comment_column) + } + Instruction::Add { dest, left, right } => { + let base = format!("{}Add r{} ← r{} + r{}", indent, dest, left, right); + let comment = format!("Arithmetic addition: r{} + r{}", left, right); + align_comment(&base, &comment, config.comment_column) + } + Instruction::Sub { dest, left, right } => { + let base = format!("{}Sub r{} ← r{} - r{}", indent, dest, left, right); + let comment = format!("Arithmetic subtraction: r{} - r{}", left, right); + align_comment(&base, &comment, config.comment_column) + } + Instruction::Mul { dest, left, right } => { + let base = format!("{}Mul r{} ← r{} × r{}", indent, dest, left, right); + let comment = format!("Arithmetic multiplication: r{} × r{}", left, right); + align_comment(&base, &comment, config.comment_column) + } + Instruction::Div { dest, left, right } => { + let base = format!("{}Div r{} ← r{} ÷ r{}", indent, dest, left, right); + let comment = format!("Arithmetic division: r{} ÷ r{}", left, right); + align_comment(&base, &comment, config.comment_column) + } + Instruction::Mod { dest, left, right } => { + let base = format!( + "{}Mod r{} ← r{} mod r{}", + indent, dest, left, right + ); + let comment = format!("Modulo operation: r{} mod r{}", left, right); + align_comment(&base, &comment, config.comment_column) + } + Instruction::Eq { dest, left, right } => { + let base = format!( + "{}Eq r{} ← (r{} = r{})", + indent, dest, left, right + ); + let comment = format!("Equality test: r{} == r{}", left, right); + align_comment(&base, &comment, config.comment_column) + } + Instruction::Ne { dest, left, right } => { + let base = format!( + "{}Ne r{} ← (r{} ≠ r{})", + indent, dest, left, right + ); + let comment = format!("Inequality test: r{} != r{}", left, right); + align_comment(&base, &comment, config.comment_column) + } + Instruction::Lt { dest, left, right } => { + let base = format!( + "{}Lt r{} ← (r{} < r{})", + indent, dest, left, right + ); + let comment = format!("Less than comparison: r{} < r{}", left, right); + align_comment(&base, &comment, config.comment_column) + } + Instruction::Le { dest, left, right } => { + let base = format!( + "{}Le r{} ← (r{} ≤ r{})", + indent, dest, left, right + ); + let comment = format!("Less or equal comparison: r{} <= r{}", left, right); + align_comment(&base, &comment, config.comment_column) + } + Instruction::Gt { dest, left, right } => { + let base = format!( + "{}Gt r{} ← (r{} > r{})", + indent, dest, left, right + ); + let comment = format!("Greater than comparison: r{} > r{}", left, right); + align_comment(&base, &comment, config.comment_column) + } + Instruction::Ge { dest, left, right } => { + let base = format!( + "{}Ge r{} ← (r{} ≥ r{})", + indent, dest, left, right + ); + let comment = format!("Greater or equal comparison: r{} >= r{}", left, right); + align_comment(&base, &comment, config.comment_column) + } + Instruction::And { dest, left, right } => { + let base = format!("{}And r{} ← r{} ∧ r{}", indent, dest, left, right); + let comment = format!("Logical AND: r{} && r{}", left, right); + align_comment(&base, &comment, config.comment_column) + } + Instruction::Or { dest, left, right } => { + let base = format!("{}Or r{} ← r{} ∨ r{}", indent, dest, left, right); + let comment = format!("Logical OR: r{} || r{}", left, right); + align_comment(&base, &comment, config.comment_column) + } + Instruction::Not { dest, operand } => { + let base = format!("{}Not r{} ← ¬r{}", indent, dest, operand); + let comment = format!("Logical NOT: !r{}", operand); + align_comment(&base, &comment, config.comment_column) + } + Instruction::BuiltinCall { params_index } => { + if let Some(params) = instruction_data.get_builtin_call_params(*params_index) { + let args_str = params + .arg_registers() + .iter() + .map(|&r| format!("r{}", r)) + .collect::>() + .join(", "); + + let builtin_name = program + .builtin_info_table + .get(params.builtin_index as usize) + .map(|info| info.name.as_str()) + .unwrap_or(""); + + let base = format!( + "{}BuiltinCall r{} ← {}({})", + indent, params.dest, builtin_name, args_str + ); + let comment = format!( + "Call builtin '{}' (B{}) with {} args", + builtin_name, params.builtin_index, params.num_args + ); + align_comment(&base, &comment, config.comment_column) + } else { + let base = format!("{}BuiltinCall [INVALID P({})]", indent, params_index); + align_comment( + &base, + "ERROR: Invalid builtin call parameters", + config.comment_column, + ) + } + } + Instruction::FunctionCall { params_index } => { + if let Some(params) = instruction_data.get_function_call_params(*params_index) { + let args_str = params + .arg_registers() + .iter() + .map(|&r| format!("r{}", r)) + .collect::>() + .join(", "); + + let func_name = program + .rule_infos + .get(params.func_rule_index as usize) + .map(|info| info.name.as_str()) + .unwrap_or(""); + + let base = format!( + "{}FunctionCall r{} ← {}({})", + indent, params.dest, func_name, args_str + ); + let comment = format!( + "Call function '{}' (R{}) with {} args", + func_name, params.func_rule_index, params.num_args + ); + align_comment(&base, &comment, config.comment_column) + } else { + let base = format!("{}FunctionCall [INVALID P({})]", indent, params_index); + align_comment( + &base, + "ERROR: Invalid function call parameters", + config.comment_column, + ) + } + } + Instruction::HostAwait { dest, arg, id } => { + let base = format!( + "{}HostAwait r{} ← await r{} (id r{})", + indent, dest, arg, id + ); + align_comment( + &base, + &format!( + "Suspend and request host result using r{} with identifier r{}", + arg, id + ), + config.comment_column, + ) + } + Instruction::Return { value } => { + let base = format!("{}Return return r{}", indent, value); + let comment = format!("Return value from r{}", value); + align_comment(&base, &comment, config.comment_column) + } + Instruction::ObjectSet { obj, key, value } => { + let base = format!("{}ObjectSet r{}[r{}] ← r{}", indent, obj, key, value); + let comment = format!("Set field r{}[r{}] = r{}", obj, key, value); + align_comment(&base, &comment, config.comment_column) + } + Instruction::ObjectCreate { params_index } => { + let params = program + .instruction_data + .get_object_create_params(*params_index); + let base = format!( + "{}ObjectCreate r{} ← {{...}}", + indent, + params.map_or(0, |p| p.dest) + ); + let comment = match params { + Some(p) => format!( + "Create object with {} fields (P{})", + p.field_count(), + params_index + ), + None => format!("Create object (P{} - INVALID)", params_index), + }; + align_comment(&base, &comment, config.comment_column) + } + Instruction::Index { + dest, + container, + key, + } => { + let base = format!( + "{}Index r{} ← r{}[r{}]", + indent, dest, container, key + ); + let comment = format!("Index operation: get r{}[r{}]", container, key); + align_comment(&base, &comment, config.comment_column) + } + Instruction::IndexLiteral { + dest, + container, + literal_idx, + } => { + let base = format!( + "{}IndexLiteral r{} ← r{}[L{}]", + indent, dest, container, literal_idx + ); + let comment = if *literal_idx < program.literals.len() as u16 { + let literal_json = serde_json::to_string(&program.literals[*literal_idx as usize]) + .unwrap_or_else(|_| "".to_string()); + format!("Index with literal key: r{}[{}]", container, literal_json) + } else { + format!( + "Index with literal: r{}[L{}] (invalid index)", + container, literal_idx + ) + }; + align_comment(&base, &comment, config.comment_column) + } + Instruction::ArrayNew { dest } => { + let base = format!("{}ArrayNew r{} ← []", indent, dest); + align_comment(&base, "Create new empty array", config.comment_column) + } + Instruction::ArrayPush { arr, value } => { + let base = format!("{}ArrayPush r{}.push(r{})", indent, arr, value); + let comment = format!("Append r{} to array r{}", value, arr); + align_comment(&base, &comment, config.comment_column) + } + Instruction::ArrayCreate { params_index } => { + if let Some(params) = instruction_data.get_array_create_params(*params_index) { + let elements = params + .element_registers() + .iter() + .map(|r| format!("r{}", r)) + .collect::>() + .join(", "); + let base = format!("{}ArrayCreate r{} ← [{}]", indent, params.dest, elements); + let comment = format!( + "Create array from {} elements (undefined if any element is undefined)", + params.element_count() + ); + align_comment(&base, &comment, config.comment_column) + } else { + format!("{}ArrayCreate ", indent, params_index) + } + } + Instruction::SetNew { dest } => { + let base = format!("{}SetNew r{} ← set()", indent, dest); + align_comment(&base, "Create new empty set", config.comment_column) + } + Instruction::SetAdd { set, value } => { + let base = format!("{}SetAdd r{} ∪= r{}", indent, set, value); + let comment = format!("Add r{} to set r{}", value, set); + align_comment(&base, &comment, config.comment_column) + } + Instruction::SetCreate { params_index } => { + if let Some(params) = instruction_data.get_set_create_params(*params_index) { + let elements = params + .element_registers() + .iter() + .map(|r| format!("r{}", r)) + .collect::>() + .join(", "); + let base = format!("{}SetCreate r{} ← {{{}}}", indent, params.dest, elements); + let comment = format!( + "Create set from {} elements (undefined if any element is undefined)", + params.element_count() + ); + align_comment(&base, &comment, config.comment_column) + } else { + format!("{}SetCreate ", indent, params_index) + } + } + Instruction::Contains { + dest, + collection, + value, + } => { + let base = format!( + "{}Contains r{} ← (r{} ∈ r{})", + indent, dest, value, collection + ); + let comment = format!("Membership test: r{} in r{}", value, collection); + align_comment(&base, &comment, config.comment_column) + } + Instruction::Count { dest, collection } => { + let base = format!("{}Count r{} ← count(r{})", indent, dest, collection); + let comment = format!("Get count/length of collection r{}", collection); + align_comment(&base, &comment, config.comment_column) + } + Instruction::AssertCondition { condition } => { + let base = format!("{}Assert assert r{}", indent, condition); + let comment = format!("Assert r{} is true (exit if false/undefined)", condition); + align_comment(&base, &comment, config.comment_column) + } + Instruction::AssertNotUndefined { register } => { + let base = format!( + "{}AssertNotUndefined assert_not_undefined r{}", + indent, register + ); + let comment = format!("Assert r{} is not undefined (exit if undefined)", register); + align_comment(&base, &comment, config.comment_column) + } + Instruction::LoopStart { params_index } => { + if let Some(params) = instruction_data.get_loop_params(*params_index) { + let mode_str = match params.mode { + LoopMode::Any => "any", + LoopMode::Every => "every", + LoopMode::ForEach => "foreach", + }; + let base = format!( + "{}LoopStart {} r{},r{} in r{} → r{} {{", + indent, + mode_str, + params.key_reg, + params.value_reg, + params.collection, + params.result_reg + ); + let comment = format!( + "{} loop over r{}, body: {}-{} (P{})", + mode_str, params.collection, params.body_start, params.loop_end, params_index + ); + align_comment(&base, &comment, config.comment_column) + } else { + let base = format!("{}LoopStart [INVALID P({})] {{", indent, params_index); + align_comment( + &base, + "ERROR: Invalid loop parameters", + config.comment_column, + ) + } + } + Instruction::LoopNext { + body_start, + loop_end, + } => { + let base = format!( + "{}}} continue → {} or exit → {}", + indent, body_start, loop_end + ); + let comment = format!( + "Next iteration or exit loop (body:{}-{})", + body_start, loop_end + ); + align_comment(&base, &comment, config.comment_column) + } + Instruction::CallRule { dest, rule_index } => { + let rule_name = program + .rule_infos + .get(*rule_index as usize) + .map(|info| info.name.as_str()) + .unwrap_or(""); + + let base = format!("{}CallRule r{} ← {}", indent, dest, rule_name); + let comment = format!("Call rule '{}' (R{}) with caching", rule_name, rule_index); + align_comment(&base, &comment, config.comment_column) + } + Instruction::RuleInit { + result_reg, + rule_index, + } => { + let rule_name = program + .rule_infos + .get(*rule_index as usize) + .map(|info| info.name.as_str()) + .unwrap_or(""); + + let base = format!("{}RuleInit {} → r{} {{", indent, rule_name, result_reg); + let comment = format!( + "Initialize rule '{}' (R{}) evaluation", + rule_name, rule_index + ); + align_comment(&base, &comment, config.comment_column) + } + Instruction::RuleReturn {} => { + let base = format!("{}}} return from rule", indent); + align_comment(&base, "End of rule evaluation", config.comment_column) + } + Instruction::ChainedIndex { params_index } => { + let (base, comment) = + if let Some(params) = instruction_data.get_chained_index_params(*params_index) { + let chain_parts: Vec = params + .path_components + .iter() + .map(|component| match component { + crate::rvm::instructions::LiteralOrRegister::Literal(idx) => { + if let Some(literal) = program.literals.get(*idx as usize) { + match literal { + crate::Value::String(s) => format!(".{}", s.as_ref()), + other => format!( + "[{}]", + serde_json::to_string(other) + .unwrap_or_else(|_| "?".to_string()) + ), + } + } else { + format!("[L{}?]", idx) + } + } + crate::rvm::instructions::LiteralOrRegister::Register(reg) => { + format!("[r{}]", reg) + } + }) + .collect(); + + let chain_display = if chain_parts.is_empty() { + String::new() + } else { + format!(" r{}{}", params.root, chain_parts.join("")) + }; + + let base_str = format!( + "{}ChainedIndex r{} ← r{}{}", + indent, params.dest, params.root, chain_display + ); + let comment_str = format!( + "Multi-level chained indexing: r{} → r{}", + params.root, params.dest + ); + (base_str, comment_str) + } else { + let base_str = format!("{}ChainedIndex chained_index", indent); + let comment_str = "Multi-level chained indexing (invalid params)".to_string(); + (base_str, comment_str) + }; + + align_comment(&base, &comment, config.comment_column) + } + Instruction::VirtualDataDocumentLookup { .. } => { + let base = format!( + "{}VirtualDataDocumentLookup virtual_data_document_lookup", + indent + ); + align_comment( + &base, + "Lookup in data namespace virtual documents", + config.comment_column, + ) + } + Instruction::DestructuringSuccess {} => { + let base = format!("{}DestructuringSuccess ✓", indent); + align_comment( + &base, + "Parameter destructuring validated", + config.comment_column, + ) + } + Instruction::Halt {} => { + let base = format!("{}Halt halt", indent); + align_comment(&base, "Stop execution", config.comment_column) + } + Instruction::ComprehensionBegin { params_index } => { + if let Some(params) = instruction_data.get_comprehension_begin_params(*params_index) { + let mode_str = match params.mode { + crate::rvm::instructions::ComprehensionMode::Array => "array", + crate::rvm::instructions::ComprehensionMode::Set => "set", + crate::rvm::instructions::ComprehensionMode::Object => "object", + }; + let (source_desc, result_desc) = if params.collection_reg == params.result_reg { + ( + format!("r{}", params.collection_reg), + format!("r{}", params.result_reg), + ) + } else { + ( + format!("r{} (src)", params.collection_reg), + format!("r{} (dst)", params.result_reg), + ) + }; + let base = format!( + "{}CompBegin {} {} → {} k:{} v:{} {{", + indent, mode_str, source_desc, result_desc, params.key_reg, params.value_reg + ); + let comment = format!( + "{} comprehension in r{}, body: {}-{} (P{})", + mode_str, + params.collection_reg, + params.body_start, + params.comprehension_end, + params_index + ); + align_comment(&base, &comment, config.comment_column) + } else { + let base = format!("{}CompBegin [INVALID P({})] {{", indent, params_index); + align_comment( + &base, + "ERROR: Invalid comprehension parameters", + config.comment_column, + ) + } + } + Instruction::ComprehensionYield { value_reg, key_reg } => { + let base = match key_reg { + Some(k) => format!("{}CompYield r{} r{}", indent, k, value_reg), + None => format!("{}CompYield r{}", indent, value_reg), + }; + align_comment(&base, "Yield value to comprehension", config.comment_column) + } + Instruction::ComprehensionEnd {} => { + let base = format!("{}}} CompEnd", indent); + align_comment(&base, "End comprehension block", config.comment_column) + } + } +} + +/// Generate compact tabular assembly listing +pub fn generate_tabular_assembly_listing( + program: &Program, + _config: &AssemblyListingConfig, +) -> String { + let mut output = String::new(); + let mut indent_level: usize = 0; + + // Add header + writeln!(output, "; RVM Assembly (Tabular Format)").unwrap(); + writeln!( + output, + "; {} instructions, {} literals", + program.instructions.len(), + program.literals.len() + ) + .unwrap(); + writeln!(output, ";").unwrap(); + writeln!(output, "; PC | Instruction | Operation").unwrap(); + writeln!(output, ";-----|--------------|----------").unwrap(); + + for (pc, instruction) in program.instructions.iter().enumerate() { + // Handle loop indentation + match instruction { + Instruction::LoopNext { .. } => { + indent_level = indent_level.saturating_sub(1); + } + Instruction::RuleReturn { .. } => { + indent_level = indent_level.saturating_sub(1); + } + _ => {} + } + + let indent = " ".repeat(indent_level * 2); // Smaller indent for tabular format + + // Format in tabular style + let addr_str = format!("{:03}", pc); + let inst_name = get_instruction_name(instruction); + let operation = + format_operation_compact(instruction, &indent, &program.instruction_data, program); + + writeln!(output, "{:>4} | {:12} | {}", addr_str, inst_name, operation).unwrap(); + + // Increase indentation for loop/rule starts + match instruction { + Instruction::LoopStart { .. } => { + indent_level += 1; + } + Instruction::RuleInit { .. } => { + indent_level += 1; + } + _ => {} + } + } + + output +} + +fn get_instruction_name(instruction: &Instruction) -> &'static str { + match instruction { + Instruction::Load { .. } => "LOAD", + Instruction::LoadTrue { .. } => "LOAD_TRUE", + Instruction::LoadFalse { .. } => "LOAD_FALSE", + Instruction::LoadNull { .. } => "LOAD_NULL", + Instruction::LoadBool { .. } => "LOAD_BOOL", + Instruction::LoadData { .. } => "LOAD_DATA", + Instruction::LoadInput { .. } => "LOAD_INPUT", + Instruction::Move { .. } => "MOVE", + Instruction::Add { .. } => "ADD", + Instruction::Sub { .. } => "SUB", + Instruction::Mul { .. } => "MUL", + Instruction::Div { .. } => "DIV", + Instruction::Mod { .. } => "MOD", + Instruction::Eq { .. } => "EQ", + Instruction::Ne { .. } => "NE", + Instruction::Lt { .. } => "LT", + Instruction::Le { .. } => "LE", + Instruction::Gt { .. } => "GT", + Instruction::Ge { .. } => "GE", + Instruction::And { .. } => "AND", + Instruction::Or { .. } => "OR", + Instruction::Not { .. } => "NOT", + Instruction::BuiltinCall { .. } => "BUILTIN_CALL", + Instruction::FunctionCall { .. } => "FUNC_CALL", + Instruction::HostAwait { .. } => "HOST_AWAIT", + Instruction::Return { .. } => "RETURN", + Instruction::ObjectSet { .. } => "OBJ_SET", + Instruction::ObjectCreate { .. } => "OBJ_CREATE", + Instruction::Index { .. } => "INDEX", + Instruction::IndexLiteral { .. } => "INDEX_LIT", + Instruction::ArrayNew { .. } => "ARRAY_NEW", + Instruction::ArrayPush { .. } => "ARRAY_PUSH", + Instruction::ArrayCreate { .. } => "ARRAY_CREATE", + Instruction::SetNew { .. } => "SET_NEW", + Instruction::SetAdd { .. } => "SET_ADD", + Instruction::SetCreate { .. } => "SET_CREATE", + Instruction::Contains { .. } => "CONTAINS", + Instruction::Count { .. } => "COUNT", + Instruction::AssertCondition { .. } => "ASSERT", + Instruction::AssertNotUndefined { .. } => "ASSERT_NOT_UNDEF", + Instruction::LoopStart { .. } => "LOOP_START", + Instruction::LoopNext { .. } => "LOOP_NEXT", + Instruction::CallRule { .. } => "CALL_RULE", + Instruction::RuleInit { .. } => "RULE_INIT", + Instruction::RuleReturn { .. } => "RULE_RET", + Instruction::DestructuringSuccess {} => "DESTRUCT_SUCCESS", + Instruction::ChainedIndex { .. } => "CHAINED_INDEX", + Instruction::VirtualDataDocumentLookup { .. } => "VIRTUAL_DATA_DOC_LOOKUP", + Instruction::Halt {} => "HALT", + Instruction::ComprehensionBegin { .. } => "COMP_BEGIN", + Instruction::ComprehensionYield { .. } => "COMP_YIELD", + Instruction::ComprehensionEnd {} => "COMP_END", + } +} + +fn format_operation_compact( + instruction: &Instruction, + indent: &str, + instruction_data: &InstructionData, + _program: &Program, +) -> String { + match instruction { + Instruction::Load { dest, literal_idx } => { + format!("{}r{} ← L{}", indent, dest, literal_idx) + } + Instruction::LoadInput { dest } => { + format!("{}r{} ← input", indent, dest) + } + Instruction::LoadData { dest } => { + format!("{}r{} ← data", indent, dest) + } + Instruction::Move { dest, src } => { + format!("{}r{} ← r{}", indent, dest, src) + } + Instruction::Add { dest, left, right } => { + format!("{}r{} ← r{} + r{}", indent, dest, left, right) + } + Instruction::Index { + dest, + container, + key, + } => { + format!("{}r{} ← r{}[r{}]", indent, dest, container, key) + } + Instruction::IndexLiteral { + dest, + container, + literal_idx, + } => { + format!("{}r{} ← r{}[L{}]", indent, dest, container, literal_idx) + } + Instruction::LoopStart { params_index } => { + if let Some(params) = instruction_data.get_loop_params(*params_index) { + format!( + "{}loop r{} in r{} {{", + indent, params.value_reg, params.collection + ) + } else { + format!("{}loop P({}) {{", indent, params_index) + } + } + Instruction::LoopNext { .. } => { + format!("{}}}", indent) + } + Instruction::CallRule { dest, rule_index } => { + format!("{}r{} ← rule_{}", indent, dest, rule_index) + } + Instruction::HostAwait { dest, arg, id } => { + format!("{}await r{} → r{} (id r{})", indent, arg, dest, id) + } + Instruction::RuleInit { + result_reg, + rule_index, + } => { + format!("{}rule_{} → r{} {{", indent, rule_index, result_reg) + } + Instruction::RuleReturn {} => { + format!("{}}}", indent) + } + Instruction::DestructuringSuccess {} => { + format!("{}✓ destructuring validated", indent) + } + _ => { + // For other instructions, use a simplified version + format!( + "{}{}", + indent, + instruction + .to_string() + .replace("R(", "r") + .replace(")", "") + .replace("L(", "L") + ) + } + } +} diff --git a/src/rvm/program/mod.rs b/src/rvm/program/mod.rs new file mode 100644 index 0000000..3ed8caf --- /dev/null +++ b/src/rvm/program/mod.rs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod core; +mod listing; +mod recompile; +mod rule_tree; +mod serialization; +mod types; + +pub use core::Program; +pub use listing::{ + generate_assembly_listing, generate_tabular_assembly_listing, AssemblyListingConfig, +}; +pub(crate) use serialization::value::{binaries_to_values, BinaryValue}; +pub use serialization::{DeserializationResult, VersionedProgram}; +pub use types::{ + BuiltinInfo, FunctionInfo, ProgramMetadata, RuleInfo, RuleType, SourceFile, SpanInfo, +}; diff --git a/src/rvm/program/recompile.rs b/src/rvm/program/recompile.rs new file mode 100644 index 0000000..5042b6d --- /dev/null +++ b/src/rvm/program/recompile.rs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::Program; +use alloc::string::{String, ToString}; + +impl Program { + /// Compile a partial deserialized program to a complete one + /// + /// This method takes a partial program (containing only entry_points and sources) + /// and recompiles it to create a complete program with all instructions and data. + pub fn compile_from_partial(partial_program: Program) -> Result { + if partial_program.entry_points.is_empty() { + return Err("Partial program must contain entry points".to_string()); + } + if partial_program.sources.is_empty() { + return Err("Partial program must contain sources".to_string()); + } + + Err("Recompilation from partial program is not yet implemented".to_string()) + } +} diff --git a/src/rvm/program/rule_tree.rs b/src/rvm/program/rule_tree.rs new file mode 100644 index 0000000..4b12656 --- /dev/null +++ b/src/rvm/program/rule_tree.rs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use anyhow::Result as AnyResult; + +use crate::value::Value; + +use super::Program; + +impl Program { + /// Add a rule to the rule tree + /// path: Package path components (e.g., ["p1", "p2"] for data.p1.p2.rule) + /// rule_name: Rule name (e.g., "rule") + /// rule_index: Index of the rule in rule_infos + pub fn add_rule_to_tree( + &mut self, + path: &[String], + rule_name: &str, + rule_index: usize, + ) -> AnyResult<()> { + let mut full_path = Vec::with_capacity(path.len() + 1); + full_path.extend(path.iter().map(|s| s.as_str())); + full_path.push(rule_name); + + let target = self.rule_tree.make_or_get_value_mut(&full_path)?; + *target = Value::Number(rule_index.into()); + + Ok(()) + } + + /// Check for conflicts between rule tree and data + /// Returns an error if any rule path conflicts with data paths + pub fn check_rule_data_conflicts(&self, data: &Value) -> Result<(), crate::rvm::vm::VmError> { + let actual_rule_tree = &self.rule_tree["data"]; + + match actual_rule_tree { + Value::Undefined => return Ok(()), + Value::Object(rule_obj) if rule_obj.is_empty() => return Ok(()), + _ => {} + } + + Self::check_conflicts_recursive(actual_rule_tree, data, &mut Vec::new()) + } + + fn check_conflicts_recursive( + rule_tree: &Value, + data: &Value, + current_path: &mut Vec, + ) -> Result<(), crate::rvm::vm::VmError> { + match rule_tree { + Value::Object(rule_obj) => { + for (key, rule_value) in rule_obj.iter() { + if let Value::String(key_str) = key { + current_path.push(key_str.to_string()); + + let data_value = &data[key]; + + match rule_value { + Value::Number(_) => { + if data_value != &Value::Undefined { + return Err(crate::rvm::vm::VmError::RuleDataConflict(format!( + "Conflict: rule defines path '{}' but data also provides this path", + current_path.join("."), + ))); + } + } + Value::Object(_) => { + if let Value::Object(_) = data_value { + Self::check_conflicts_recursive( + rule_value, + data_value, + current_path, + )?; + } else if data_value != &Value::Undefined { + return Err(crate::rvm::vm::VmError::RuleDataConflict(format!( + "Conflict: rule defines subpaths under '{}' but data provides a non-object value at this path", + current_path.join("."), + ))); + } + } + _ => { + return Err(crate::rvm::vm::VmError::RuleDataConflict(format!( + "Invalid rule tree structure at path '{}'", + current_path.join("."), + ))); + } + } + + current_path.pop(); + } + } + } + _ => { + return Err(crate::rvm::vm::VmError::RuleDataConflict( + "Rule tree root must be an object".to_string(), + )); + } + } + + Ok(()) + } +} diff --git a/src/rvm/program/serialization/binary.rs b/src/rvm/program/serialization/binary.rs new file mode 100644 index 0000000..1f40906 --- /dev/null +++ b/src/rvm/program/serialization/binary.rs @@ -0,0 +1,482 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use bincode::config::standard; +use bincode::serde::{decode_from_slice, encode_to_vec}; +use indexmap::IndexMap; + +use super::super::types::SourceFile; +use super::{DeserializationResult, Program}; +use crate::value::Value; + +use super::value::{ + binaries_to_values, binary_to_value, BinaryValue, BinaryValueRef, BinaryValueSlice, +}; +type ArtifactData = (IndexMap, Vec, bool); + +impl Program { + /// Serialize program to binary format. + /// Uses pure bincode for all sections now that `Value` supports serde. + pub fn serialize_binary(&self) -> Result, String> { + let mut buffer = Vec::new(); + + buffer.extend_from_slice(&Self::MAGIC); + buffer.extend_from_slice(&Self::SERIALIZATION_VERSION.to_le_bytes()); + + let entry_points_bin = encode_to_vec(&self.entry_points, standard()) + .map_err(|e| format!("Entry points bincode serialization failed: {}", e))?; + + let sources_bin = encode_to_vec(&self.sources, standard()) + .map_err(|e| format!("Sources bincode serialization failed: {}", e))?; + + let literals_bin = encode_to_vec(BinaryValueSlice(self.literals.as_slice()), standard()) + .map_err(|e| format!("Literals bincode serialization failed: {}", e))?; + + let rule_tree_bin = encode_to_vec(BinaryValueRef(&self.rule_tree), standard()) + .map_err(|e| format!("Rule tree bincode serialization failed: {}", e))?; + + let binary_data = encode_to_vec(self, standard()) + .map_err(|e| format!("Program structure binary serialization failed: {}", e))?; + + buffer.extend_from_slice(&(entry_points_bin.len() as u32).to_le_bytes()); + buffer.extend_from_slice(&(sources_bin.len() as u32).to_le_bytes()); + buffer.extend_from_slice(&(literals_bin.len() as u32).to_le_bytes()); + buffer.extend_from_slice(&(rule_tree_bin.len() as u32).to_le_bytes()); + buffer.push(if self.rego_v0 { 1 } else { 0 }); + + buffer.extend_from_slice(&entry_points_bin); + buffer.extend_from_slice(&sources_bin); + buffer.extend_from_slice(&literals_bin); + buffer.extend_from_slice(&rule_tree_bin); + + buffer.extend_from_slice(&(binary_data.len() as u32).to_le_bytes()); + buffer.extend_from_slice(&binary_data); + + Ok(buffer) + } + + /// Deserialize only the artifact section (entry_points and sources) from binary format + pub fn deserialize_artifacts_only(data: &[u8]) -> Result { + if data.len() < 9 { + return Err("Data too short for artifact header".to_string()); + } + + if data[0..4] != Self::MAGIC { + return Err("Invalid file format - magic number mismatch".to_string()); + } + + let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]); + + match version { + 1 => { + if data.len() < 17 { + return Err("Data too short for artifact header".to_string()); + } + + let entry_points_len = + u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize; + let sources_len = + u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize; + let rego_v0 = data[16] != 0; + let entry_points_start = 17; + let sources_start = entry_points_start + entry_points_len; + let sources_end = sources_start + sources_len; + + if data.len() < sources_end { + return Err("Data truncated in artifact section".to_string()); + } + + let entry_points = + decode_from_slice(&data[entry_points_start..sources_start], standard()) + .map(|(value, _)| value) + .unwrap_or_else(|_| IndexMap::new()); + + let sources = decode_from_slice(&data[sources_start..sources_end], standard()) + .map(|(value, _)| value) + .unwrap_or_else(|_| Vec::new()); + + Ok((entry_points, sources, rego_v0)) + } + 2 | 3 => { + if data.len() < 25 { + return Err("Data too short for artifact header".to_string()); + } + + let entry_points_len = + u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize; + let sources_len = + u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize; + let literals_len = + u32::from_le_bytes([data[16], data[17], data[18], data[19]]) as usize; + let rule_tree_len = + u32::from_le_bytes([data[20], data[21], data[22], data[23]]) as usize; + let rego_v0 = data[24] != 0; + + let entry_points_start = 25; + let sources_start = entry_points_start + entry_points_len; + let literals_start = sources_start + sources_len; + let rule_tree_start = literals_start + literals_len; + let rule_tree_end = rule_tree_start + rule_tree_len; + + if data.len() < rule_tree_end { + return Err("Data truncated in artifact section".to_string()); + } + + let entry_points = + decode_from_slice(&data[entry_points_start..sources_start], standard()) + .map(|(value, _)| value) + .unwrap_or_else(|_| IndexMap::new()); + + let sources = decode_from_slice(&data[sources_start..literals_start], standard()) + .map(|(value, _)| value) + .unwrap_or_else(|_| Vec::new()); + + Ok((entry_points, sources, rego_v0)) + } + v => Err(format!("Unsupported version {}", v)), + } + } + + /// Deserialize program from binary format with version checking + pub fn deserialize_binary(data: &[u8]) -> Result { + if data.len() < 9 { + return Err("Data too short for header".to_string()); + } + + if data[0..4] != Self::MAGIC { + return Err("Invalid file format - magic number mismatch".to_string()); + } + + let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]); + if version > Self::SERIALIZATION_VERSION { + return Err(format!( + "Unsupported version {}. Maximum supported version is {}", + version, + Self::SERIALIZATION_VERSION + )); + } + + match version { + 1 => { + if data.len() < 25 { + return Err("Data too short for header".to_string()); + } + + let entry_points_len = + u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize; + let sources_len = + u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize; + let rego_v0 = data[16] != 0; + let entry_points_start = 17; + let sources_start = entry_points_start + entry_points_len; + let binary_len_start = sources_start + sources_len; + + if data.len() < binary_len_start + 4 { + return Err("Data too short for binary length".to_string()); + } + + let binary_len = u32::from_le_bytes([ + data[binary_len_start], + data[binary_len_start + 1], + data[binary_len_start + 2], + data[binary_len_start + 3], + ]) as usize; + + let json_len_start = binary_len_start + 4 + binary_len; + if data.len() < json_len_start + 4 { + return Err("Data too short for JSON length".to_string()); + } + + let json_len = u32::from_le_bytes([ + data[json_len_start], + data[json_len_start + 1], + data[json_len_start + 2], + data[json_len_start + 3], + ]) as usize; + + let total_expected = json_len_start + 4 + json_len; + if data.len() < total_expected { + return Err("Data truncated".to_string()); + } + + let binary_start = binary_len_start + 4; + let json_start = json_len_start + 4; + + let entry_points = + decode_from_slice(&data[entry_points_start..sources_start], standard()) + .map(|(value, _)| value) + .map_err(|e| format!("Entry points deserialization failed: {}", e))?; + + let sources = decode_from_slice(&data[sources_start..binary_len_start], standard()) + .map(|(value, _)| value) + .map_err(|e| format!("Sources deserialization failed: {}", e))?; + + let mut needs_recompilation = false; + + let mut program = match decode_from_slice::( + &data[binary_start..json_start], + standard(), + ) { + Ok((prog, _)) => prog, + Err(_e) => { + needs_recompilation = true; + Program::new() + } + }; + + let (literals, rule_tree) = match serde_json::from_slice::( + &data[json_start..json_start + json_len], + ) { + Ok(combined) => { + let literals = combined + .get("literals") + .and_then(|v| serde_json::from_value::>(v.clone()).ok()) + .unwrap_or_else(|| { + needs_recompilation = true; + Vec::new() + }); + + let rule_tree = combined + .get("rule_tree") + .and_then(|v| serde_json::from_value::(v.clone()).ok()) + .unwrap_or_else(|| { + needs_recompilation = true; + Value::new_object() + }); + + (literals, rule_tree) + } + Err(_e) => { + needs_recompilation = true; + (Vec::new(), Value::new_object()) + } + }; + + program.entry_points = entry_points; + program.sources = sources; + program.literals = literals; + program.rule_tree = rule_tree; + program.rego_v0 = rego_v0; + program.needs_recompilation = needs_recompilation; + + if !program.builtin_info_table.is_empty() { + if let Err(_e) = program.initialize_resolved_builtins() { + program.needs_recompilation = true; + } + } + + if program.needs_recompilation { + Ok(DeserializationResult::Partial(program)) + } else { + Ok(DeserializationResult::Complete(program)) + } + } + 2 | 3 => { + if data.len() < 29 { + return Err("Data too short for header".to_string()); + } + + let entry_points_len = + u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize; + let sources_len = + u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize; + let literals_len = + u32::from_le_bytes([data[16], data[17], data[18], data[19]]) as usize; + let rule_tree_len = + u32::from_le_bytes([data[20], data[21], data[22], data[23]]) as usize; + let rego_v0 = data[24] != 0; + + let entry_points_start = 25; + let sources_start = entry_points_start + entry_points_len; + let literals_start = sources_start + sources_len; + let rule_tree_start = literals_start + literals_len; + let binary_len_start = rule_tree_start + rule_tree_len; + + if data.len() < binary_len_start + 4 { + return Err("Data too short for binary length".to_string()); + } + + let binary_len = u32::from_le_bytes([ + data[binary_len_start], + data[binary_len_start + 1], + data[binary_len_start + 2], + data[binary_len_start + 3], + ]) as usize; + + let binary_start = binary_len_start + 4; + let binary_end = binary_start + binary_len; + + if data.len() < binary_end { + return Err("Data truncated".to_string()); + } + + let entry_points = + decode_from_slice(&data[entry_points_start..sources_start], standard()) + .map(|(value, _)| value) + .map_err(|e| format!("Entry points deserialization failed: {}", e))?; + + let sources = decode_from_slice(&data[sources_start..literals_start], standard()) + .map(|(value, _)| value) + .map_err(|e| format!("Sources deserialization failed: {}", e))?; + + let mut needs_recompilation = false; + + let literals = match decode_from_slice::, _>( + &data[literals_start..rule_tree_start], + standard(), + ) { + Ok((binary_literals, _)) => match binaries_to_values(binary_literals) { + Ok(values) => values, + Err(_e) => { + needs_recompilation = true; + Vec::new() + } + }, + Err(_e) => { + needs_recompilation = true; + Vec::new() + } + }; + + let rule_tree = match decode_from_slice::( + &data[rule_tree_start..binary_len_start], + standard(), + ) { + Ok((binary_tree, _)) => match binary_to_value(binary_tree) { + Ok(value) => value, + Err(_e) => { + needs_recompilation = true; + Value::new_object() + } + }, + Err(_e) => { + needs_recompilation = true; + Value::new_object() + } + }; + + let mut program = match decode_from_slice::( + &data[binary_start..binary_end], + standard(), + ) { + Ok((prog, _)) => prog, + Err(_e) => { + needs_recompilation = true; + Program::new() + } + }; + + program.entry_points = entry_points; + program.sources = sources; + program.literals = literals; + program.rule_tree = rule_tree; + program.rego_v0 = rego_v0; + program.needs_recompilation = needs_recompilation; + + if !program.builtin_info_table.is_empty() { + if let Err(_e) = program.initialize_resolved_builtins() { + program.needs_recompilation = true; + } + } + + if program.needs_recompilation { + Ok(DeserializationResult::Partial(program)) + } else { + Ok(DeserializationResult::Complete(program)) + } + } + v => Err(format!("Unsupported version {}", v)), + } + } + + /// Check if data can be deserialized without actually deserializing + pub fn can_deserialize(data: &[u8]) -> Result { + if data.len() < 8 { + return Ok(false); + } + + if data[0..4] != Self::MAGIC { + return Ok(false); + } + + let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]); + + match version { + 1..=3 => Ok(true), + _ => Ok(false), + } + } + + /// Get file format information without deserializing + pub fn get_file_info(data: &[u8]) -> Result<(u32, usize), String> { + if data.len() < 9 { + return Err("Data too short for header".to_string()); + } + + if data[0..4] != Self::MAGIC { + return Err("Invalid file format".to_string()); + } + + let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]); + + match version { + 1 => { + if data.len() < 25 { + return Err("Data too short for header".to_string()); + } + + let entry_points_len = + u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize; + let sources_len = + u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize; + let binary_len_start = 17 + entry_points_len + sources_len; + + if data.len() < binary_len_start + 4 { + return Err("Data too short for binary length".to_string()); + } + + let binary_len = u32::from_le_bytes([ + data[binary_len_start], + data[binary_len_start + 1], + data[binary_len_start + 2], + data[binary_len_start + 3], + ]) as usize; + + Ok((version, binary_len)) + } + 2 | 3 => { + if data.len() < 29 { + return Err("Data too short for header".to_string()); + } + + let entry_points_len = + u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize; + let sources_len = + u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize; + let literals_len = + u32::from_le_bytes([data[16], data[17], data[18], data[19]]) as usize; + let rule_tree_len = + u32::from_le_bytes([data[20], data[21], data[22], data[23]]) as usize; + let binary_len_start = + 25 + entry_points_len + sources_len + literals_len + rule_tree_len; + + if data.len() < binary_len_start + 4 { + return Err("Data too short for binary length".to_string()); + } + + let binary_len = u32::from_le_bytes([ + data[binary_len_start], + data[binary_len_start + 1], + data[binary_len_start + 2], + data[binary_len_start + 3], + ]) as usize; + + Ok((version, binary_len)) + } + v => Err(format!("Unsupported version {}", v)), + } + } +} diff --git a/src/rvm/program/serialization/json.rs b/src/rvm/program/serialization/json.rs new file mode 100644 index 0000000..97063fe --- /dev/null +++ b/src/rvm/program/serialization/json.rs @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; + +use super::super::types::SourceFile; +use super::super::types::{BuiltinInfo, ProgramMetadata, RuleInfo, SpanInfo}; +use super::Program; +use crate::rvm::instructions::InstructionData; +use crate::rvm::Instruction; +use crate::value::Value; +use indexmap::IndexMap; + +impl Program { + /// Serialize to JSON format with complete program information and proper field names + pub fn serialize_json(&self) -> Result { + let json_data = serde_json::json!({ + "metadata": { + "compiler_version": self.metadata.compiler_version, + "compiled_at": self.metadata.compiled_at, + "source_info": self.metadata.source_info, + "optimization_level": self.metadata.optimization_level, + "rego_v0": self.rego_v0, + "needs_runtime_recursion_check": self.needs_runtime_recursion_check, + "needs_recompilation": self.needs_recompilation + }, + "program_structure": { + "main_entry_point": self.main_entry_point, + "max_rule_window_size": self.max_rule_window_size, + "dispatch_window_size": self.dispatch_window_size, + }, + "instructions": self.instructions, + "instruction_data": { + "loop_params": self.instruction_data.loop_params, + "builtin_call_params": self.instruction_data.builtin_call_params, + "function_call_params": self.instruction_data.function_call_params, + "object_create_params": self.instruction_data.object_create_params, + "array_create_params": self.instruction_data.array_create_params, + "set_create_params": self.instruction_data.set_create_params, + "virtual_data_document_lookup_params": self.instruction_data.virtual_data_document_lookup_params, + "chained_index_params": self.instruction_data.chained_index_params, + "comprehension_begin_params": self.instruction_data.comprehension_begin_params + }, + "literals": self.literals, + "builtin_info_table": self.builtin_info_table, + "entry_points": self.entry_points, + "sources": self.sources, + "rule_infos": self.rule_infos, + "instruction_spans": self.instruction_spans, + "rule_tree": self.rule_tree + }); + + serde_json::to_string_pretty(&json_data) + .map_err(|e| format!("JSON serialization failed: {}", e)) + } + + /// Deserialize program from JSON format + pub fn deserialize_json(data: &str) -> Result { + let json_data: serde_json::Value = + serde_json::from_str(data).map_err(|e| format!("JSON parsing failed: {}", e))?; + + let metadata = json_data + .get("metadata") + .ok_or("Missing metadata section")?; + let compiler_version = metadata + .get("compiler_version") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") + .to_string(); + let compiled_at = metadata + .get("compiled_at") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") + .to_string(); + let source_info = metadata + .get("source_info") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let optimization_level = metadata + .get("optimization_level") + .and_then(|v| v.as_u64()) + .unwrap_or(0) as u8; + let rego_v0 = metadata + .get("rego_v0") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let needs_runtime_recursion_check = metadata + .get("needs_runtime_recursion_check") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let needs_recompilation = metadata + .get("needs_recompilation") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let program_structure = json_data + .get("program_structure") + .ok_or("Missing program_structure section")?; + let main_entry_point = program_structure + .get("main_entry_point") + .and_then(|v| v.as_u64()) + .unwrap_or(0) as usize; + let max_rule_window_size = program_structure + .get("max_rule_window_size") + .and_then(|v| v.as_u64()) + .unwrap_or(0) as usize; + let dispatch_window_size = program_structure + .get("dispatch_window_size") + .and_then(|v| v.as_u64()) + .unwrap_or(0) as usize; + + let instructions: Vec = serde_json::from_value( + json_data + .get("instructions") + .ok_or("Missing instructions section")? + .clone(), + ) + .map_err(|e| format!("Failed to deserialize instructions: {}", e))?; + + let instruction_data_json = json_data + .get("instruction_data") + .ok_or("Missing instruction_data section")?; + let instruction_data: InstructionData = + serde_json::from_value(instruction_data_json.clone()) + .map_err(|e| format!("Failed to deserialize instruction_data: {}", e))?; + + let literals: Vec = json_data + .get("literals") + .map(|v| serde_json::from_value(v.clone()).unwrap_or_default()) + .unwrap_or_default(); + + let builtin_info_table: Vec = json_data + .get("builtin_info_table") + .map(|v| serde_json::from_value(v.clone()).unwrap_or_default()) + .unwrap_or_default(); + + let entry_points: IndexMap = json_data + .get("entry_points") + .map(|v| serde_json::from_value(v.clone()).unwrap_or_default()) + .unwrap_or_default(); + + let sources: Vec = json_data + .get("sources") + .map(|v| serde_json::from_value(v.clone()).unwrap_or_default()) + .unwrap_or_default(); + + let rule_infos: Vec = json_data + .get("rule_infos") + .map(|v| serde_json::from_value(v.clone()).unwrap_or_default()) + .unwrap_or_default(); + + let instruction_spans: Vec> = json_data + .get("instruction_spans") + .map(|v| serde_json::from_value(v.clone()).unwrap_or_default()) + .unwrap_or_default(); + + let rule_tree: Value = json_data + .get("rule_tree") + .map(|v| serde_json::from_value(v.clone()).unwrap_or_else(|_| Value::new_object())) + .unwrap_or_else(Value::new_object); + + let mut program = Program { + instructions, + literals, + instruction_data, + builtin_info_table, + entry_points, + sources, + rule_infos, + instruction_spans, + main_entry_point, + max_rule_window_size, + dispatch_window_size, + metadata: ProgramMetadata { + compiler_version, + compiled_at, + source_info, + optimization_level, + }, + rule_tree, + resolved_builtins: Vec::new(), + needs_runtime_recursion_check, + needs_recompilation, + rego_v0, + }; + + if !program.builtin_info_table.is_empty() { + let _ = program.initialize_resolved_builtins(); + } + + Ok(program) + } +} diff --git a/src/rvm/program/serialization/mod.rs b/src/rvm/program/serialization/mod.rs new file mode 100644 index 0000000..b74066a --- /dev/null +++ b/src/rvm/program/serialization/mod.rs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +pub(crate) mod binary; +mod json; +pub(crate) mod value; + +use serde::{Deserialize, Serialize}; + +use super::Program; + +/// Versioned program wrapper for serialization compatibility +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VersionedProgram { + /// Format version for compatibility checking + pub version: u32, + /// The actual program data + pub program: Program, +} + +/// Result of program deserialization that explicitly indicates completeness +#[derive(Debug, Clone)] +pub enum DeserializationResult { + /// Full deserialization was successful - program is fully functional + Complete(Program), + /// Only artifact section was deserialized - extensible sections failed + /// The program contains entry_points and sources but requires recompilation + Partial(Program), +} diff --git a/src/rvm/program/serialization/value.rs b/src/rvm/program/serialization/value.rs new file mode 100644 index 0000000..96fd37d --- /dev/null +++ b/src/rvm/program/serialization/value.rs @@ -0,0 +1,294 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::format; +use alloc::string::String; +use alloc::vec::Vec; +use core::fmt; +use core::str::FromStr; +use serde::de::{self, EnumAccess, VariantAccess, Visitor}; +use serde::ser::{SerializeSeq, SerializeTuple}; +use serde::{Deserialize, Serialize}; + +use crate::number::Number; +use crate::value::Value; + +const VARIANT_NULL: u32 = 0; +const VARIANT_BOOL: u32 = 1; +const VARIANT_NUMBER_STRING: u32 = 2; +const VARIANT_STRING: u32 = 3; +const VARIANT_ARRAY: u32 = 4; +const VARIANT_SET: u32 = 5; +const VARIANT_OBJECT: u32 = 6; +const VARIANT_UNDEFINED: u32 = 7; +const VARIANT_NUMBER_I64: u32 = 8; +const VARIANT_NUMBER_U64: u32 = 9; +const VARIANT_NUMBER_F64: u32 = 10; + +/// Wrapper type for zero-copy binary serialization of a `Value`. +/// Keeps references into the original data so collections and strings are not cloned. +pub(crate) struct BinaryValueRef<'a>(pub &'a Value); + +impl<'a> Serialize for BinaryValueRef<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self.0 { + Value::Null => serializer.serialize_unit_variant("BinaryValue", VARIANT_NULL, "Null"), + Value::Bool(b) => { + serializer.serialize_newtype_variant("BinaryValue", VARIANT_BOOL, "Bool", &b) + } + Value::Number(n) => { + if let Some(value) = n.as_i64() { + serializer.serialize_newtype_variant( + "BinaryValue", + VARIANT_NUMBER_I64, + "NumberI64", + &value, + ) + } else if let Some(value) = n.as_u64() { + serializer.serialize_newtype_variant( + "BinaryValue", + VARIANT_NUMBER_U64, + "NumberU64", + &value, + ) + } else if let Some(value) = n.as_f64() { + serializer.serialize_newtype_variant( + "BinaryValue", + VARIANT_NUMBER_F64, + "NumberF64", + &value, + ) + } else { + serializer.serialize_newtype_variant( + "BinaryValue", + VARIANT_NUMBER_STRING, + "Number", + &n.format_scientific(), + ) + } + } + Value::String(s) => serializer.serialize_newtype_variant( + "BinaryValue", + VARIANT_STRING, + "String", + s.as_ref(), + ), + Value::Array(items) => serializer.serialize_newtype_variant( + "BinaryValue", + VARIANT_ARRAY, + "Array", + &BinaryValueSlice(items.as_slice()), + ), + Value::Set(items) => serializer.serialize_newtype_variant( + "BinaryValue", + VARIANT_SET, + "Set", + &BinarySetRef(items.as_ref()), + ), + Value::Object(entries) => serializer.serialize_newtype_variant( + "BinaryValue", + VARIANT_OBJECT, + "Object", + &BinaryObjectRef(entries.as_ref()), + ), + Value::Undefined => { + serializer.serialize_unit_variant("BinaryValue", VARIANT_UNDEFINED, "Undefined") + } + } + } +} + +/// Slice wrapper allowing zero-copy serialization of value collections. +pub(crate) struct BinaryValueSlice<'a>(pub &'a [Value]); + +impl<'a> Serialize for BinaryValueSlice<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut seq = serializer.serialize_seq(Some(self.0.len()))?; + for value in self.0 { + seq.serialize_element(&BinaryValueRef(value))?; + } + seq.end() + } +} + +struct BinarySetRef<'a>(&'a BTreeSet); + +impl<'a> Serialize for BinarySetRef<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut seq = serializer.serialize_seq(Some(self.0.len()))?; + for value in self.0.iter() { + seq.serialize_element(&BinaryValueRef(value))?; + } + seq.end() + } +} + +struct BinaryObjectRef<'a>(&'a BTreeMap); + +impl<'a> Serialize for BinaryObjectRef<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut seq = serializer.serialize_seq(Some(self.0.len()))?; + for (key, value) in self.0.iter() { + seq.serialize_element(&BinaryEntryRef(key, value))?; + } + seq.end() + } +} + +struct BinaryEntryRef<'a>(&'a Value, &'a Value); + +impl<'a> Serialize for BinaryEntryRef<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut tuple = serializer.serialize_tuple(2)?; + tuple.serialize_element(&BinaryValueRef(self.0))?; + tuple.serialize_element(&BinaryValueRef(self.1))?; + tuple.end() + } +} + +/// Owned counterpart used during deserialization. +#[derive(Debug, Clone)] +pub(crate) struct BinaryValue(pub Value); + +impl BinaryValue { + fn into_value(self) -> Value { + self.0 + } +} + +const BINARY_VARIANTS: &[&str] = &[ + "Null", + "Bool", + "Number", + "String", + "Array", + "Set", + "Object", + "Undefined", + "NumberI64", + "NumberU64", + "NumberF64", +]; + +#[derive(Deserialize)] +#[serde(rename_all = "PascalCase")] +enum BinaryVariant { + Null, + Bool, + Number, + String, + Array, + Set, + Object, + Undefined, + NumberI64, + NumberU64, + NumberF64, +} + +struct BinaryValueVisitor; + +impl<'de> Visitor<'de> for BinaryValueVisitor { + type Value = BinaryValue; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a BinaryValue enum") + } + + fn visit_enum(self, data: A) -> Result + where + A: EnumAccess<'de>, + { + match data.variant()? { + (BinaryVariant::Null, variant) => { + variant.unit_variant()?; + Ok(BinaryValue(Value::Null)) + } + (BinaryVariant::Bool, variant) => { + let value = variant.newtype_variant::()?; + Ok(BinaryValue(Value::from(value))) + } + (BinaryVariant::Number, variant) => { + let numeric = variant.newtype_variant::<&'de str>()?; + let number = Number::from_str(numeric).map_err(|_| { + de::Error::custom(format!("Invalid numeric string '{numeric}'")) + })?; + Ok(BinaryValue(Value::from(number))) + } + (BinaryVariant::NumberI64, variant) => { + let value = variant.newtype_variant::()?; + Ok(BinaryValue(Value::from(value))) + } + (BinaryVariant::NumberU64, variant) => { + let value = variant.newtype_variant::()?; + Ok(BinaryValue(Value::from(value))) + } + (BinaryVariant::NumberF64, variant) => { + let value = variant.newtype_variant::()?; + Ok(BinaryValue(Value::from(value))) + } + (BinaryVariant::String, variant) => { + let s = variant.newtype_variant::<&'de str>()?; + Ok(BinaryValue(Value::from(s))) + } + (BinaryVariant::Array, variant) => { + let items: Vec = variant.newtype_variant()?; + let values: Vec = items.into_iter().map(BinaryValue::into_value).collect(); + Ok(BinaryValue(Value::from(values))) + } + (BinaryVariant::Set, variant) => { + let items: Vec = variant.newtype_variant()?; + let mut set = BTreeSet::new(); + for item in items { + set.insert(item.into_value()); + } + Ok(BinaryValue(Value::from(set))) + } + (BinaryVariant::Object, variant) => { + let entries: Vec<(BinaryValue, BinaryValue)> = variant.newtype_variant()?; + let mut map = BTreeMap::new(); + for (key, value) in entries { + map.insert(key.into_value(), value.into_value()); + } + Ok(BinaryValue(Value::from(map))) + } + (BinaryVariant::Undefined, variant) => { + variant.unit_variant()?; + Ok(BinaryValue(Value::Undefined)) + } + } + } +} + +impl<'de> Deserialize<'de> for BinaryValue { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_enum("BinaryValue", BINARY_VARIANTS, BinaryValueVisitor) + } +} + +pub(crate) fn binaries_to_values(binaries: Vec) -> Result, String> { + Ok(binaries.into_iter().map(BinaryValue::into_value).collect()) +} + +pub(crate) fn binary_to_value(binary: BinaryValue) -> Result { + Ok(binary.into_value()) +} diff --git a/src/rvm/program/types.rs b/src/rvm/program/types.rs new file mode 100644 index 0000000..966c31d --- /dev/null +++ b/src/rvm/program/types.rs @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use alloc::string::String; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; + +/// Builtin function information stored in program's builtin info table +#[repr(C)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BuiltinInfo { + /// Builtin function name + pub name: String, + /// Exact number of arguments required + pub num_args: u16, +} + +/// Span information for debugging and error reporting +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SpanInfo { + /// Index into the source table + pub source_index: usize, + /// Line number (1-based) + pub line: usize, + /// Column number (1-based) + pub column: usize, + /// Length of the span + pub length: usize, +} + +impl SpanInfo { + pub fn new(source_index: usize, line: usize, column: usize, length: usize) -> Self { + Self { + source_index, + line, + column, + length, + } + } + + /// Create SpanInfo from lexer Span with source table lookup + pub fn from_lexer_span(span: &crate::lexer::Span, source_index: usize) -> Self { + Self { + source_index, + line: span.line as usize, + column: span.col as usize, + length: span.text().len(), + } + } + + /// Get source information using the program's source table + pub fn get_source<'a>(&self, source_table: &'a [SourceFile]) -> Option<&'a str> { + source_table + .get(self.source_index) + .map(|s| s.content.as_str()) + } + + /// Get source name using the program's source table + pub fn get_source_name<'a>(&self, source_table: &'a [SourceFile]) -> Option<&'a str> { + source_table.get(self.source_index).map(|s| s.name.as_str()) + } +} + +/// Rule type enumeration for different kinds of rules (complete, partial set, partial object) +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)] +pub enum RuleType { + Complete, + PartialSet, + PartialObject, +} + +/// Information about function rules +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FunctionInfo { + /// Parameter names in order + pub param_names: Vec, + /// Number of parameters + pub num_params: u32, +} + +/// Rule metadata for debugging and introspection +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RuleInfo { + /// Rule name (e.g., "data.package.rule_name") + pub name: String, + /// Rule type + pub rule_type: RuleType, + /// Definitions + pub definitions: crate::Rc>>, + /// Function-specific information (only present for function rules) + pub function_info: Option, + /// Index into the program's literal table for default value (only for Complete rules) + pub default_literal_index: Option, + /// Register allocated for this rule's result accumulation + pub result_reg: u8, + /// Number of registers used by this rule (for register windowing) + pub num_registers: u8, + /// Optional destructuring block entry point per definition + /// Index: definition_index → Some(entry_point) | None + pub destructuring_blocks: Vec>, +} + +impl RuleInfo { + pub fn new( + name: String, + rule_type: RuleType, + definitions: crate::Rc>>, + result_reg: u8, + num_registers: u8, + ) -> Self { + let num_definitions = definitions.len(); + Self { + name, + rule_type, + definitions, + function_info: None, + default_literal_index: None, + result_reg, + num_registers, + destructuring_blocks: alloc::vec![None; num_definitions], + } + } + + /// Create a new function rule with parameter information + pub fn new_function( + name: String, + rule_type: RuleType, + definitions: crate::Rc>>, + param_names: Vec, + result_reg: u8, + num_registers: u8, + ) -> Self { + let num_params = param_names.len() as u32; + let num_definitions = definitions.len(); + Self { + name, + rule_type, + definitions, + function_info: Some(FunctionInfo { + param_names, + num_params, + }), + default_literal_index: None, + result_reg, + num_registers, + destructuring_blocks: alloc::vec![None; num_definitions], + } + } + + /// Set the default literal index for this rule + pub fn set_default_literal_index(&mut self, default_literal_index: u16) { + self.default_literal_index = Some(default_literal_index); + } +} + +/// Source file information containing filename and contents +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SourceFile { + /// Source file identifier/path + pub name: String, + /// The actual source code content + pub content: String, +} + +impl SourceFile { + pub fn new(name: String, content: String) -> Self { + Self { name, content } + } +} + +/// Program compilation metadata +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProgramMetadata { + /// Compiler version that generated this program + pub compiler_version: String, + /// Compilation timestamp + pub compiled_at: String, + /// Source policy information + pub source_info: String, + /// Optimization level used + pub optimization_level: u8, +} diff --git a/src/rvm/tests/instruction_parser.rs b/src/rvm/tests/instruction_parser.rs new file mode 100644 index 0000000..780e3dd --- /dev/null +++ b/src/rvm/tests/instruction_parser.rs @@ -0,0 +1,648 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::rvm::instructions::{Instruction, LoopMode}; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use anyhow::{anyhow, bail, Result}; + +/// Parse a textual instruction like "Load { dest: 0, literal_idx: 1 }" +pub fn parse_instruction(text: &str) -> Result { + let text = text.trim(); + + // Find the instruction name and parameters + if let Some(brace_start) = text.find('{') { + let name = text[..brace_start].trim(); + let params_text = &text[brace_start..]; + + match name { + "Load" => parse_load(params_text), + "LoadTrue" => parse_load_true(params_text), + "LoadFalse" => parse_load_false(params_text), + "LoadNull" => parse_load_null(params_text), + "LoadBool" => parse_load_bool(params_text), + "LoadData" => parse_load_data(params_text), + "LoadInput" => parse_load_input(params_text), + "Move" => parse_move(params_text), + "Add" => parse_add(params_text), + "Sub" => parse_sub(params_text), + "Mul" => parse_mul(params_text), + "Div" => parse_div(params_text), + "Mod" => parse_mod(params_text), + "Eq" => parse_eq(params_text), + "Ne" => parse_ne_instruction(params_text), + "Lt" => parse_lt(params_text), + "Le" => parse_le_instruction(params_text), + "Gt" => parse_gt(params_text), + "Ge" => parse_ge_instruction(params_text), + "And" => parse_and(params_text), + "Or" => parse_or(params_text), + "Not" => parse_not(params_text), + "Return" => parse_return(params_text), + "RuleInit" => parse_rule_init(params_text), + "RuleReturn" => parse_rule_return(params_text), + "DestructuringSuccess" => parse_destructuring_success(params_text), + "ObjectSet" => parse_object_set(params_text), + "ObjectCreate" => parse_object_create(params_text), + "Index" => parse_index(params_text), + "IndexLiteral" => parse_index_literal(params_text), + "ChainedIndex" => parse_chained_index(params_text), + "ArrayNew" => parse_array_new(params_text), + "ArrayCreate" => parse_array_create(params_text), + "SetCreate" => parse_set_create(params_text), + "ArrayPush" => parse_array_push(params_text), + "SetNew" => parse_set_new(params_text), + "SetAdd" => parse_set_add(params_text), + "Contains" => parse_contains(params_text), + "Count" => parse_count(params_text), + "AssertCondition" => parse_assert_condition(params_text), + "AssertNotUndefined" => parse_assert_not_undefined(params_text), + "BuiltinCall" => parse_builtin_call(params_text), + "FunctionCall" => parse_function_call(params_text), + "CallRule" => parse_call_rule(params_text), + "VirtualDataDocumentLookup" => parse_virtual_data_document_lookup(params_text), + "HostAwait" => parse_host_await(params_text), + "LoopStart" => parse_loop_start(params_text), + "LoopNext" => parse_loop_next(params_text), + "ComprehensionStart" => parse_comprehension_start(params_text), + "ComprehensionAdd" => parse_comprehension_add(params_text), + "ComprehensionBegin" => parse_comprehension_start(params_text), + "ComprehensionYield" => parse_comprehension_add(params_text), + _ => bail!("Unknown instruction: {}", name), + } + } else { + // Handle instructions without parameters (no braces) + let name = text.trim(); + match name { + "Halt" => Ok(Instruction::Halt {}), + "RuleReturn" => Ok(Instruction::RuleReturn {}), + "DestructuringSuccess" => Ok(Instruction::DestructuringSuccess {}), + "ComprehensionEnd" => Ok(Instruction::ComprehensionEnd {}), + _ => bail!("Unknown instruction: {}", name), + } + } +} + +// Parameter parsing helpers +fn parse_params(text: &str) -> Result> { + if !text.starts_with('{') || !text.ends_with('}') { + bail!("Parameters must be enclosed in braces"); + } + + let inner = &text[1..text.len() - 1]; + let mut params = Vec::new(); + let mut current = String::new(); + let in_value = false; + let mut colon_pos = None; + + for ch in inner.chars() { + match ch { + ':' if !in_value => { + colon_pos = Some(current.len()); + current.push(ch); + } + ',' if !in_value => { + if let Some(pos) = colon_pos { + let key = current[..pos].trim().to_string(); + let value = current[pos + 1..].trim().to_string(); + params.push((key, value)); + current.clear(); + colon_pos = None; + } else { + bail!("Invalid parameter format"); + } + } + _ => current.push(ch), + } + } + + // Handle the last parameter + if !current.trim().is_empty() { + if let Some(pos) = colon_pos { + let key = current[..pos].trim().to_string(); + let value = current[pos + 1..].trim().to_string(); + params.push((key, value)); + } else { + bail!("Invalid parameter format"); + } + } + + Ok(params) +} + +fn get_param_u16(params: &[(String, String)], name: &str) -> Result { + for (key, value) in params { + if key == name { + return value + .parse::() + .map_err(|_| anyhow!("Invalid u16 value for {}: {}", name, value)); + } + } + bail!("Missing parameter: {}", name); +} + +fn get_param_bool(params: &[(String, String)], name: &str) -> Result { + for (key, value) in params { + if key == name { + return value + .parse::() + .map_err(|_| anyhow!("Invalid bool value for {}: {}", name, value)); + } + } + bail!("Missing parameter: {}", name); +} + +pub fn parse_loop_mode(text: &str) -> Result { + match text { + "Any" => Ok(LoopMode::Any), + "Every" => Ok(LoopMode::Every), + "ForEach" => Ok(LoopMode::ForEach), + // Keep backwards compatibility for now + "Existential" => Ok(LoopMode::Any), + "Universal" => Ok(LoopMode::Every), + "Collect" => Ok(LoopMode::ForEach), + // Legacy comprehension modes now map to ForEach since we use dedicated comprehension instructions + "ArrayComprehension" => Ok(LoopMode::ForEach), + "SetComprehension" => Ok(LoopMode::ForEach), + "ObjectComprehension" => Ok(LoopMode::ForEach), + _ => bail!("Invalid loop mode: {}", text), + } +} + +// Individual instruction parsers +fn parse_load(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let literal_idx = get_param_u16(¶ms, "literal_idx")?; + Ok(Instruction::Load { + dest: dest.try_into().unwrap(), + literal_idx, + }) +} + +fn parse_move(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let src = get_param_u16(¶ms, "src")?; + Ok(Instruction::Move { + dest: dest.try_into().unwrap(), + src: src.try_into().unwrap(), + }) +} + +fn parse_add(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let left = get_param_u16(¶ms, "left")?; + let right = get_param_u16(¶ms, "right")?; + Ok(Instruction::Add { + dest: dest.try_into().unwrap(), + left: left.try_into().unwrap(), + right: right.try_into().unwrap(), + }) +} + +fn parse_sub(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let left = get_param_u16(¶ms, "left")?; + let right = get_param_u16(¶ms, "right")?; + Ok(Instruction::Sub { + dest: dest.try_into().unwrap(), + left: left.try_into().unwrap(), + right: right.try_into().unwrap(), + }) +} + +fn parse_mul(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let left = get_param_u16(¶ms, "left")?; + let right = get_param_u16(¶ms, "right")?; + Ok(Instruction::Mul { + dest: dest.try_into().unwrap(), + left: left.try_into().unwrap(), + right: right.try_into().unwrap(), + }) +} + +fn parse_div(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let left = get_param_u16(¶ms, "left")?; + let right = get_param_u16(¶ms, "right")?; + Ok(Instruction::Div { + dest: dest.try_into().unwrap(), + left: left.try_into().unwrap(), + right: right.try_into().unwrap(), + }) +} + +fn parse_eq(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let left = get_param_u16(¶ms, "left")?; + let right = get_param_u16(¶ms, "right")?; + Ok(Instruction::Eq { + dest: dest.try_into().unwrap(), + left: left.try_into().unwrap(), + right: right.try_into().unwrap(), + }) +} + +fn parse_ne_instruction(content: &str) -> Result { + let params = parse_params(content)?; + let dest = get_param_u16(¶ms, "dest")?; + let left = get_param_u16(¶ms, "left")?; + let right = get_param_u16(¶ms, "right")?; + Ok(Instruction::Ne { + dest: dest.try_into().unwrap(), + left: left.try_into().unwrap(), + right: right.try_into().unwrap(), + }) +} + +fn parse_lt(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let left = get_param_u16(¶ms, "left")?; + let right = get_param_u16(¶ms, "right")?; + Ok(Instruction::Lt { + dest: dest.try_into().unwrap(), + left: left.try_into().unwrap(), + right: right.try_into().unwrap(), + }) +} + +fn parse_le_instruction(content: &str) -> Result { + let params = parse_params(content)?; + let dest = get_param_u16(¶ms, "dest")?; + let left = get_param_u16(¶ms, "left")?; + let right = get_param_u16(¶ms, "right")?; + Ok(Instruction::Le { + dest: dest.try_into().unwrap(), + left: left.try_into().unwrap(), + right: right.try_into().unwrap(), + }) +} + +fn parse_gt(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let left = get_param_u16(¶ms, "left")?; + let right = get_param_u16(¶ms, "right")?; + Ok(Instruction::Gt { + dest: dest.try_into().unwrap(), + left: left.try_into().unwrap(), + right: right.try_into().unwrap(), + }) +} + +fn parse_ge_instruction(content: &str) -> Result { + let params = parse_params(content)?; + let dest = get_param_u16(¶ms, "dest")?; + let left = get_param_u16(¶ms, "left")?; + let right = get_param_u16(¶ms, "right")?; + Ok(Instruction::Ge { + dest: dest.try_into().unwrap(), + left: left.try_into().unwrap(), + right: right.try_into().unwrap(), + }) +} + +fn parse_return(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let value = get_param_u16(¶ms, "value")?; + Ok(Instruction::Return { + value: value.try_into().unwrap(), + }) +} + +fn parse_rule_init(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let result_reg = get_param_u16(¶ms, "result_reg")?; + let rule_index = get_param_u16(¶ms, "rule_index")?; + Ok(Instruction::RuleInit { + result_reg: result_reg.try_into().unwrap(), + rule_index, + }) +} + +fn parse_rule_return(params_text: &str) -> Result { + let _params = parse_params(params_text)?; + Ok(Instruction::RuleReturn {}) +} + +fn parse_destructuring_success(params_text: &str) -> Result { + let _params = parse_params(params_text)?; + Ok(Instruction::DestructuringSuccess {}) +} + +fn parse_object_set(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let obj = get_param_u16(¶ms, "obj")?; + let key = get_param_u16(¶ms, "key")?; + let value = get_param_u16(¶ms, "value")?; + Ok(Instruction::ObjectSet { + obj: obj.try_into().unwrap(), + key: key.try_into().unwrap(), + value: value.try_into().unwrap(), + }) +} + +fn parse_object_create(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let params_index = get_param_u16(¶ms, "params_index")?; + Ok(Instruction::ObjectCreate { params_index }) +} + +fn parse_index(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let container = get_param_u16(¶ms, "container")?; + let key = get_param_u16(¶ms, "key")?; + Ok(Instruction::Index { + dest: dest.try_into().unwrap(), + container: container.try_into().unwrap(), + key: key.try_into().unwrap(), + }) +} + +fn parse_index_literal(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let container = get_param_u16(¶ms, "container")?; + let literal_idx = get_param_u16(¶ms, "literal_idx")?; + Ok(Instruction::IndexLiteral { + dest: dest.try_into().unwrap(), + container: container.try_into().unwrap(), + literal_idx, + }) +} + +fn parse_chained_index(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let params_index = get_param_u16(¶ms, "params_index")?; + Ok(Instruction::ChainedIndex { params_index }) +} + +fn parse_array_new(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + Ok(Instruction::ArrayNew { + dest: dest.try_into().unwrap(), + }) +} + +fn parse_array_push(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let arr = get_param_u16(¶ms, "arr")?; + let value = get_param_u16(¶ms, "value")?; + Ok(Instruction::ArrayPush { + arr: arr.try_into().unwrap(), + value: value.try_into().unwrap(), + }) +} + +fn parse_array_create(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let params_index = get_param_u16(¶ms, "params_index")?; + Ok(Instruction::ArrayCreate { params_index }) +} + +fn parse_set_create(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let params_index = get_param_u16(¶ms, "params_index")?; + Ok(Instruction::SetCreate { params_index }) +} + +fn parse_set_new(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + Ok(Instruction::SetNew { + dest: dest.try_into().unwrap(), + }) +} + +fn parse_set_add(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let set = get_param_u16(¶ms, "set")?; + let value = get_param_u16(¶ms, "value")?; + Ok(Instruction::SetAdd { + set: set.try_into().unwrap(), + value: value.try_into().unwrap(), + }) +} + +fn parse_contains(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let collection = get_param_u16(¶ms, "collection")?; + let value = get_param_u16(¶ms, "value")?; + Ok(Instruction::Contains { + dest: dest.try_into().unwrap(), + collection: collection.try_into().unwrap(), + value: value.try_into().unwrap(), + }) +} + +fn parse_count(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let collection = get_param_u16(¶ms, "collection")?; + Ok(Instruction::Count { + dest: dest.try_into().unwrap(), + collection: collection.try_into().unwrap(), + }) +} + +fn parse_assert_condition(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let condition = get_param_u16(¶ms, "condition")?; + Ok(Instruction::AssertCondition { + condition: condition.try_into().unwrap(), + }) +} + +fn parse_assert_not_undefined(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let register = get_param_u16(¶ms, "register")?; + Ok(Instruction::AssertNotUndefined { + register: register.try_into().unwrap(), + }) +} + +fn parse_loop_start(params_text: &str) -> Result { + let params = parse_params(params_text)?; + + // Get params_index parameter - this should be specified in the test + let params_index = get_param_u16(¶ms, "params_index")?; + + Ok(Instruction::LoopStart { params_index }) +} + +fn parse_loop_next(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let body_start = get_param_u16(¶ms, "body_start")?; + let loop_end = get_param_u16(¶ms, "loop_end")?; + Ok(Instruction::LoopNext { + body_start, + loop_end, + }) +} + +fn parse_load_true(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + Ok(Instruction::LoadTrue { + dest: dest.try_into().unwrap(), + }) +} + +fn parse_load_false(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + Ok(Instruction::LoadFalse { + dest: dest.try_into().unwrap(), + }) +} + +fn parse_load_null(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + Ok(Instruction::LoadNull { + dest: dest.try_into().unwrap(), + }) +} + +fn parse_load_bool(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let value = get_param_bool(¶ms, "value")?; + Ok(Instruction::LoadBool { + dest: dest.try_into().unwrap(), + value, + }) +} + +fn parse_load_data(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + Ok(Instruction::LoadData { + dest: dest.try_into().unwrap(), + }) +} + +fn parse_load_input(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + Ok(Instruction::LoadInput { + dest: dest.try_into().unwrap(), + }) +} + +fn parse_mod(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let left = get_param_u16(¶ms, "left")?; + let right = get_param_u16(¶ms, "right")?; + Ok(Instruction::Mod { + dest: dest.try_into().unwrap(), + left: left.try_into().unwrap(), + right: right.try_into().unwrap(), + }) +} + +fn parse_and(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let left = get_param_u16(¶ms, "left")?; + let right = get_param_u16(¶ms, "right")?; + Ok(Instruction::And { + dest: dest.try_into().unwrap(), + left: left.try_into().unwrap(), + right: right.try_into().unwrap(), + }) +} + +fn parse_or(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let left = get_param_u16(¶ms, "left")?; + let right = get_param_u16(¶ms, "right")?; + Ok(Instruction::Or { + dest: dest.try_into().unwrap(), + left: left.try_into().unwrap(), + right: right.try_into().unwrap(), + }) +} + +fn parse_not(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let operand = get_param_u16(¶ms, "operand")?; + Ok(Instruction::Not { + dest: dest.try_into().unwrap(), + operand: operand.try_into().unwrap(), + }) +} + +fn parse_builtin_call(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let params_index = get_param_u16(¶ms, "params_index")?; + Ok(Instruction::BuiltinCall { params_index }) +} + +fn parse_function_call(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let params_index = get_param_u16(¶ms, "params_index")?; + Ok(Instruction::FunctionCall { params_index }) +} + +fn parse_call_rule(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let rule_index = get_param_u16(¶ms, "rule_index")?; + Ok(Instruction::CallRule { + dest: dest.try_into().unwrap(), + rule_index, + }) +} + +fn parse_virtual_data_document_lookup(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let params_index = get_param_u16(¶ms, "params_index")?; + Ok(Instruction::VirtualDataDocumentLookup { params_index }) +} + +fn parse_host_await(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest = get_param_u16(¶ms, "dest")?; + let arg = get_param_u16(¶ms, "arg")?; + let id = get_param_u16(¶ms, "id")?; + Ok(Instruction::HostAwait { + dest: dest.try_into().unwrap(), + arg: arg.try_into().unwrap(), + id: id.try_into().unwrap(), + }) +} + +fn parse_comprehension_start(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let params_index = get_param_u16(¶ms, "params_index")?; + Ok(Instruction::ComprehensionBegin { params_index }) +} + +fn parse_comprehension_add(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let value_reg = get_param_u16(¶ms, "value_reg")?; + let key_reg = if let Ok(key) = get_param_u16(¶ms, "key_reg") { + Some(key.try_into().unwrap()) + } else { + None + }; + Ok(Instruction::ComprehensionYield { + value_reg: value_reg.try_into().unwrap(), + key_reg, + }) +} diff --git a/src/rvm/tests/mod.rs b/src/rvm/tests/mod.rs new file mode 100644 index 0000000..67cab66 --- /dev/null +++ b/src/rvm/tests/mod.rs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! RVM test modules + +#[cfg(test)] +pub mod instruction_parser; + +pub mod test_utils; + +#[cfg(test)] +pub mod vm; diff --git a/src/rvm/tests/test_utils.rs b/src/rvm/tests/test_utils.rs new file mode 100644 index 0000000..c86e6e4 --- /dev/null +++ b/src/rvm/tests/test_utils.rs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Test utility functions for RVM serialization + +use crate::rvm::program::{binaries_to_values, BinaryValue, Program}; +use alloc::format; +use alloc::string::String; +use alloc::vec::Vec; +use bincode::config::standard; +use bincode::serde::decode_from_slice; + +/// Test utility function for round-trip serialization +/// Serializes program, deserializes it, and serializes again to check for consistency +pub fn test_round_trip_serialization(program: &Program) -> Result<(), String> { + // First serialization + let serialized1 = program.serialize_binary()?; + + // Basic validation: ensure literal section decodes cleanly under the current format. + if serialized1.len() >= 8 && serialized1.starts_with(&Program::MAGIC) { + let version = u32::from_le_bytes([ + serialized1[4], + serialized1[5], + serialized1[6], + serialized1[7], + ]); + + if version == 2 && serialized1.len() >= 25 { + let entry_points_len = u32::from_le_bytes([ + serialized1[8], + serialized1[9], + serialized1[10], + serialized1[11], + ]) as usize; + let sources_len = u32::from_le_bytes([ + serialized1[12], + serialized1[13], + serialized1[14], + serialized1[15], + ]) as usize; + let literals_len = u32::from_le_bytes([ + serialized1[16], + serialized1[17], + serialized1[18], + serialized1[19], + ]) as usize; + let entry_points_start = 25; + let sources_start = entry_points_start + entry_points_len; + let literals_start = sources_start + sources_len; + let rule_tree_start = literals_start + literals_len; + + if literals_len > 0 && serialized1.len() >= rule_tree_start { + match decode_from_slice::, _>( + &serialized1[literals_start..rule_tree_start], + standard(), + ) { + Ok((decoded_literals, _)) => { + if binaries_to_values(decoded_literals).is_err() { + return Err( + "Failed to convert literal table from binary representation".into(), + ); + } + } + Err(err) => { + return Err(format!( + "Failed to decode literal table with bincode: {}", + err + )); + } + } + } + } + } + + // Deserialize + let deserialized = match Program::deserialize_binary(&serialized1)? { + crate::rvm::program::DeserializationResult::Complete(program) => program, + crate::rvm::program::DeserializationResult::Partial(program) => { + let info = format!( + "Deserialization resulted in partial program during round-trip test \ + (instructions={}, literals={}, needs_recompilation={})", + program.instructions.len(), + program.literals.len(), + program.needs_recompilation() + ); + return Err(info); + } + }; + + // Second serialization + let serialized2 = deserialized.serialize_binary()?; + + // Compare the two serialized versions + if serialized1 == serialized2 { + Ok(()) + } else { + Err(format!( + "Round-trip serialization failed: serialized data differs. \ + First serialization: {} bytes, Second: {} bytes", + serialized1.len(), + serialized2.len() + )) + } +} diff --git a/src/rvm/tests/vm.rs b/src/rvm/tests/vm.rs new file mode 100644 index 0000000..7c78570 --- /dev/null +++ b/src/rvm/tests/vm.rs @@ -0,0 +1,995 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[cfg(test)] +mod tests { + use crate::rvm::tests::instruction_parser::{parse_instruction, parse_loop_mode}; + use crate::rvm::tests::test_utils::test_round_trip_serialization; + #[derive(Debug, Clone, Deserialize, Serialize, Default)] + struct RuleInfoSpec { + rule_type: String, + definitions: Vec>, + #[serde(default)] + default_rule_index: Option, + #[serde(default)] + default_literal_index: Option, + #[serde(default)] + destructuring_blocks: Option>>, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + struct DefaultRuleSpec { + rule_name: String, + default_value: crate::Value, + } + + use crate::rvm::vm::{ExecutionMode, ExecutionState, RegoVM, SuspendReason, VmError}; + use crate::tests::interpreter::process_value; + use crate::value::Value; + use alloc::collections::{BTreeMap, VecDeque}; + use alloc::string::{String, ToString}; + use alloc::sync::Arc; + use alloc::vec::Vec; + use anyhow::Result; + use serde::{Deserialize, Serialize}; + use std::fs; + use test_generator::test_resources; + + extern crate alloc; + extern crate std; + + #[derive(Debug, Clone, Deserialize, Serialize)] + struct HostAwaitResponseSpec { + id: crate::Value, + #[serde(default)] + value: Option, + #[serde(default)] + values: Vec, + } + + #[derive(Debug, Deserialize, Serialize)] + struct VmTestCase { + note: String, + #[serde(default)] + description: Option, + #[serde(default)] + example_rego: Option, + #[serde(default)] + data: Option, + #[serde(default)] + input: Option, + literals: Vec, + #[serde(default)] + rule_infos: Vec, + #[serde(default)] + default_rules: Vec, + #[serde(default)] + rule_tree: Option, + #[serde(default)] + instruction_params: Option, + #[serde(default)] + max_instructions: Option, + #[serde(default)] + host_await_responses: Option>, + #[serde(default)] + host_await_responses_run_to_completion: Option>, + #[serde(default)] + host_await_responses_suspendable: Option>, + #[serde(default)] + ignore_run_to_completion_hostawait_failure: bool, + instructions: Vec, + #[serde(default, deserialize_with = "deserialize_optional_value")] + want_result: Option, + #[serde(default)] + want_error: Option, + #[serde(default, deserialize_with = "deserialize_optional_value")] + want_result_strict: Option, + #[serde(default)] + want_error_strict: Option, + } + + fn deserialize_optional_value<'de, D>(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + // If the field is present, always return Some, even if the value is null + crate::Value::deserialize(deserializer).map(Some) + } + + #[derive(Debug, Clone, Deserialize, Serialize, Default)] + struct InstructionParamsSpec { + #[serde(default)] + loop_params: Vec, + #[serde(default)] + call_params: Vec, + #[serde(default)] + builtin_call_params: Vec, + #[serde(default)] + function_call_params: Vec, + #[serde(default)] + builtin_infos: Vec, + #[serde(default)] + object_create_params: Vec, + #[serde(default)] + array_create_params: Vec, + #[serde(default)] + set_create_params: Vec, + #[serde(default)] + virtual_data_document_lookup_params: Vec, + #[serde(default)] + chained_index_params: Vec, + #[serde(default, alias = "comprehension_start_params")] + comprehension_begin_params: Vec, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + struct LoopStartParamsSpec { + mode: String, + collection: u16, + key_reg: u16, + value_reg: u16, + result_reg: u16, + body_start: u16, + loop_end: u16, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + struct CallParamsSpec { + dest: u16, + func: u16, + args_start: u16, + args_count: u16, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + struct BuiltinCallParamsSpec { + dest: u16, + builtin_index: u16, + args: Vec, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + struct FunctionCallParamsSpec { + func: u16, + dest: u16, + args: Vec, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + struct BuiltinInfoSpec { + name: String, + num_args: u16, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + struct ObjectCreateParamsSpec { + dest: u16, + template_literal_idx: u16, + literal_key_fields: Vec<(u16, u16)>, + fields: Vec<(u16, u16)>, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + struct ArrayCreateParamsSpec { + dest: u16, + elements: Vec, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + struct SetCreateParamsSpec { + dest: u16, + elements: Vec, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + struct LiteralOrRegisterSpec { + #[serde(default, alias = "literal")] + literal_idx: Option, + #[serde(default, alias = "reg")] + register: Option, + } + + impl LiteralOrRegisterSpec { + fn into_literal_or_register(self) -> crate::rvm::instructions::LiteralOrRegister { + if let Some(literal_idx) = self.literal_idx { + crate::rvm::instructions::LiteralOrRegister::Literal(literal_idx) + } else if let Some(register) = self.register { + crate::rvm::instructions::LiteralOrRegister::Register(register.try_into().unwrap()) + } else { + panic!("LiteralOrRegisterSpec must specify either literal_idx or register"); + } + } + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + struct VirtualDataDocumentLookupParamsSpec { + dest: u16, + path_components: Vec, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + struct ChainedIndexParamsSpec { + dest: u16, + root: u16, + path_components: Vec, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + struct ComprehensionBeginParamsSpec { + mode: String, + collection_reg: u16, + #[serde(default)] + result_reg: Option, + key_reg: u16, + value_reg: u16, + body_start: u16, + comprehension_end: u16, + } + + #[derive(Debug, Deserialize, Serialize)] + struct VmTestSuite { + cases: Vec, + } + + /// Execute VM instructions directly from parsed instructions and literals + #[allow(clippy::too_many_arguments)] + fn execute_vm_instructions( + instructions: Vec, + literals: Vec, + rule_infos: Vec, + rule_tree: Option, + instruction_params: Option, + data: Option, + input: Option, + max_instructions: Option, + host_await_responses: Option>, + host_await_responses_run_to_completion: Option>, + host_await_responses_suspendable: Option>, + ignore_run_to_completion_hostawait_failure: bool, + strict: bool, + ) -> Result { + let processed_data = if let Some(ref data_value) = data { + Some(process_value(data_value)?) + } else { + None + }; + + let processed_input = if let Some(ref input_value) = input { + Some(process_value(input_value)?) + } else { + None + }; + + let processed_rule_tree = if let Some(ref tree_value) = rule_tree { + Some(process_value(tree_value)?) + } else { + None + }; + + let process_responses = + |responses: Vec| -> Result>> { + let mut processed: BTreeMap> = BTreeMap::new(); + + for spec in responses { + let identifier = process_value(&spec.id)?; + let mut values: Vec = Vec::new(); + + if let Some(single) = spec.value.as_ref() { + values.push(process_value(single)?); + } + + for value in &spec.values { + values.push(process_value(value)?); + } + + if values.is_empty() { + return Err(anyhow::anyhow!( + "HostAwait response specification for id {:?} has no values", + identifier + )); + } + + processed.entry(identifier).or_default().extend(values); + } + + Ok(processed) + }; + + let processed_host_responses = if let Some(responses) = host_await_responses { + Some(process_responses(responses)?) + } else { + None + }; + + let processed_host_responses_run_to_completion = + if let Some(responses) = host_await_responses_run_to_completion { + Some(process_responses(responses)?) + } else { + processed_host_responses.clone() + }; + + let processed_host_responses_suspendable = + if let Some(responses) = host_await_responses_suspendable { + Some(process_responses(responses)?) + } else { + processed_host_responses.clone() + }; + + // Create a Program from instructions and literals + let mut program = crate::rvm::program::Program::new(); + program.instructions = instructions; + + // Process literals through the value converter to handle special syntax like set! + let mut processed_literals = Vec::new(); + for literal in &literals { + processed_literals.push(process_value(literal)?); + } + program.literals = processed_literals; + + if let Some(tree) = processed_rule_tree { + program.rule_tree = tree; + } else { + program.rule_tree = Value::new_object(); + } + + // Convert rule infos + for rule_info_spec in rule_infos.iter() { + use crate::rvm::program::{RuleInfo, RuleType}; + + let rule_type = match rule_info_spec.rule_type.as_str() { + "Complete" => RuleType::Complete, + "PartialSet" => RuleType::PartialSet, + "PartialObject" => RuleType::PartialObject, + _ => { + return Err(anyhow::anyhow!( + "Unknown rule type: {}", + rule_info_spec.rule_type + )) + } + }; + + // Convert Vec> to Vec> + let definitions: Vec> = rule_info_spec + .definitions + .iter() + .map(|def| def.iter().map(|&x| x as u32).collect()) + .collect(); + + let mut destructuring_blocks: Vec> = rule_info_spec + .destructuring_blocks + .clone() + .map(|blocks| { + blocks + .into_iter() + .map(|entry| entry.map(|value| value as u32)) + .collect() + }) + .unwrap_or_else(|| alloc::vec![None; definitions.len()]); + + if destructuring_blocks.len() != definitions.len() { + destructuring_blocks.resize(definitions.len(), None); + } + + // For function calls, use result_reg 0; for other rules, use result_reg 1 + let result_reg = if instruction_params + .as_ref() + .is_some_and(|params| !params.function_call_params.is_empty()) + { + 0 // Function calls use register 0 as return register + } else { + 1 // Regular rules use register 1 + }; + + let rule_info = RuleInfo { + name: String::from("test_rule"), + rule_type, + definitions: crate::Rc::new(definitions.clone()), + function_info: None, + default_literal_index: rule_info_spec.default_literal_index, + result_reg, + num_registers: 50, // Increased to accommodate test cases with higher register indices + destructuring_blocks, + }; + + program.rule_infos.push(rule_info); + } + + // Build instruction data from params specification + if let Some(params_spec) = instruction_params { + // Convert loop params + for loop_param_spec in params_spec.loop_params { + let mode = parse_loop_mode(&loop_param_spec.mode)?; + let loop_params = crate::rvm::instructions::LoopStartParams { + mode, + collection: loop_param_spec.collection.try_into().unwrap(), + key_reg: loop_param_spec.key_reg.try_into().unwrap(), + value_reg: loop_param_spec.value_reg.try_into().unwrap(), + result_reg: loop_param_spec.result_reg.try_into().unwrap(), + body_start: loop_param_spec.body_start, + loop_end: loop_param_spec.loop_end, + }; + program.add_loop_params(loop_params); + } + + // Convert call params + // Legacy call_params support removed - use builtin_call_params or function_call_params instead + if !params_spec.call_params.is_empty() { + // Legacy call parameters are no longer supported + // Convert to BuiltinCall or FunctionCall instructions instead + panic!("Legacy call_params are no longer supported. Use builtin_call_params or function_call_params instead."); + } + + // Convert builtin info specs to program builtin info table + for builtin_info_spec in params_spec.builtin_infos { + let builtin_info = crate::rvm::program::BuiltinInfo { + name: builtin_info_spec.name, + num_args: builtin_info_spec.num_args, + }; + program.add_builtin_info(builtin_info); + } + + // Convert builtin call params + for builtin_call_spec in params_spec.builtin_call_params { + use crate::rvm::instructions::BuiltinCallParams; + + // Convert Vec to fixed array (unused slots are irrelevant due to num_args) + let mut args_array = [0u8; 8]; + for (i, &arg) in builtin_call_spec.args.iter().enumerate() { + if i < 8 { + args_array[i] = arg.try_into().unwrap(); + } + } + + let builtin_call_params = BuiltinCallParams { + dest: builtin_call_spec.dest.try_into().unwrap(), + builtin_index: builtin_call_spec.builtin_index, + num_args: builtin_call_spec.args.len() as u8, + args: args_array, + }; + program.add_builtin_call_params(builtin_call_params); + } + + // Convert function call params + for function_call_spec in params_spec.function_call_params { + use crate::rvm::instructions::FunctionCallParams; + + // Convert Vec to fixed array (unused slots are irrelevant due to num_args) + let mut args_array = [0u8; 8]; + for (i, &arg) in function_call_spec.args.iter().enumerate() { + if i < 8 { + args_array[i] = arg.try_into().unwrap(); + } + } + + let function_call_params = FunctionCallParams { + func_rule_index: function_call_spec.func, + dest: function_call_spec.dest.try_into().unwrap(), + num_args: function_call_spec.args.len() as u8, + args: args_array, + }; + program.add_function_call_params(function_call_params); + } + + // Convert object create params + for object_create_spec in params_spec.object_create_params { + use crate::rvm::instructions::ObjectCreateParams; + + let object_create_params = ObjectCreateParams { + dest: object_create_spec.dest.try_into().unwrap(), + template_literal_idx: object_create_spec.template_literal_idx, + literal_key_fields: object_create_spec + .literal_key_fields + .into_iter() + .map(|(k, v)| (k, v.try_into().unwrap())) + .collect(), + fields: object_create_spec + .fields + .into_iter() + .map(|(k, v)| (k.try_into().unwrap(), v.try_into().unwrap())) + .collect(), + }; + program + .instruction_data + .add_object_create_params(object_create_params); + } + + // Convert array create params + for array_create_spec in params_spec.array_create_params { + use crate::rvm::instructions::ArrayCreateParams; + + let array_create_params = ArrayCreateParams { + dest: array_create_spec.dest.try_into().unwrap(), + elements: array_create_spec + .elements + .into_iter() + .map(|reg| reg.try_into().unwrap()) + .collect(), + }; + program + .instruction_data + .add_array_create_params(array_create_params); + } + + // Convert set create params + for set_create_spec in params_spec.set_create_params { + use crate::rvm::instructions::SetCreateParams; + + let set_create_params = SetCreateParams { + dest: set_create_spec.dest.try_into().unwrap(), + elements: set_create_spec + .elements + .into_iter() + .map(|reg| reg.try_into().unwrap()) + .collect(), + }; + program + .instruction_data + .add_set_create_params(set_create_params); + } + + // Convert virtual data document lookup params + for virtual_spec in params_spec.virtual_data_document_lookup_params { + use crate::rvm::instructions::{ + LiteralOrRegister, VirtualDataDocumentLookupParams, + }; + + let path_components: Vec = virtual_spec + .path_components + .into_iter() + .map(|component| component.into_literal_or_register()) + .collect(); + + let params = VirtualDataDocumentLookupParams { + dest: virtual_spec.dest.try_into().unwrap(), + path_components, + }; + + program + .instruction_data + .add_virtual_data_document_lookup_params(params); + } + + // Convert chained index params + for chained_spec in params_spec.chained_index_params { + use crate::rvm::instructions::{ChainedIndexParams, LiteralOrRegister}; + + let path_components: Vec = chained_spec + .path_components + .into_iter() + .map(|component| component.into_literal_or_register()) + .collect(); + + let params = ChainedIndexParams { + dest: chained_spec.dest.try_into().unwrap(), + root: chained_spec.root.try_into().unwrap(), + path_components, + }; + + program.instruction_data.add_chained_index_params(params); + } + + // Convert comprehension start params + for comprehension_spec in params_spec.comprehension_begin_params { + use crate::rvm::instructions::{ComprehensionBeginParams, ComprehensionMode}; + + let mode = match comprehension_spec.mode.as_str() { + "Array" => ComprehensionMode::Array, + "Set" => ComprehensionMode::Set, + "Object" => ComprehensionMode::Object, + _ => panic!("Invalid comprehension mode: {}", comprehension_spec.mode), + }; + + let comprehension_params = ComprehensionBeginParams { + mode, + collection_reg: comprehension_spec.collection_reg.try_into().unwrap(), + result_reg: comprehension_spec + .result_reg + .unwrap_or(comprehension_spec.collection_reg) + .try_into() + .unwrap(), + key_reg: comprehension_spec.key_reg.try_into().unwrap(), + value_reg: comprehension_spec.value_reg.try_into().unwrap(), + body_start: comprehension_spec.body_start, + comprehension_end: comprehension_spec.comprehension_end, + }; + program + .instruction_data + .add_comprehension_begin_params(comprehension_params); + } + } + + program.main_entry_point = 0; + + // Set a reasonable default for register window size in VM tests + // Most tests use registers 0-10, so we'll allocate 256 registers to be safe + program.max_rule_window_size = 256; + program.dispatch_window_size = 50; + + // Initialize resolved builtins if we have builtin info + if !program.builtin_info_table.is_empty() { + if let Err(e) = program.initialize_resolved_builtins() { + return Err(anyhow::anyhow!( + "Failed to initialize resolved builtins: {}", + e + )); + } + } + + // Ensure program artifacts survive binary round-tripping + test_round_trip_serialization(&program) + .map_err(|e| anyhow::anyhow!("Program serialization round-trip failed: {}", e))?; + + let program = Arc::new(program); + + let run_with_mode = |mode: ExecutionMode, + use_step_mode: bool, + host_responses_template: Option>>| + -> Result> { + let mut vm = RegoVM::new(); + vm.set_execution_mode(mode); + vm.set_step_mode(use_step_mode); + vm.set_strict_builtin_errors(strict); + + if let Some(data_value) = processed_data.clone() { + vm.set_data(data_value)?; + } + if let Some(input_value) = processed_input.clone() { + vm.set_input(input_value); + } + + if let Some(limit) = max_instructions { + vm.set_max_instructions(limit); + } + + if matches!(mode, ExecutionMode::RunToCompletion) { + if let Some(responses) = host_responses_template.clone() { + vm.set_host_await_responses(responses); + } + } + + vm.load_program(program.clone()); + + let mut response_map = host_responses_template.clone().map(|map| { + map.into_iter() + .map(|(identifier, values)| (identifier, VecDeque::from(values))) + .collect::>() + }); + + let mut last_result = vm.execute().map_err(|e| anyhow::anyhow!("{}", e)); + + loop { + match vm.execution_state() { + ExecutionState::Completed { result } => { + return Ok(Ok(result.clone())); + } + ExecutionState::Error { error } => { + return Ok(Err(anyhow::anyhow!("{}", error))); + } + ExecutionState::Suspended { reason, .. } => { + if mode != ExecutionMode::Suspendable { + return Ok(Err(anyhow::anyhow!( + "Run-to-completion execution unexpectedly suspended: {:?}", + reason + ))); + } + + match reason { + SuspendReason::HostAwait { + dest, identifier, .. + } => { + let dest = *dest; + let identifier = identifier.clone(); + + let response = { + let map = response_map.as_mut().ok_or_else(|| { + anyhow::anyhow!( + "{}", + VmError::HostAwaitResponseMissing { + dest, + identifier: identifier.clone(), + } + ) + })?; + + let queue = map.get_mut(&identifier).ok_or_else(|| { + anyhow::anyhow!( + "{}", + VmError::HostAwaitResponseMissing { + dest, + identifier: identifier.clone(), + } + ) + })?; + + let response = queue.pop_front().ok_or_else(|| { + anyhow::anyhow!( + "{}", + VmError::HostAwaitResponseMissing { + dest, + identifier: identifier.clone(), + } + ) + })?; + + if queue.is_empty() { + map.remove(&identifier); + } + + response + }; + + last_result = vm + .resume(Some(response)) + .map_err(|e| anyhow::anyhow!("{}", e)); + } + SuspendReason::Step => { + if !use_step_mode { + return Ok(Err(anyhow::anyhow!( + "Suspendable execution unexpectedly suspended: {:?}", + reason + ))); + } + last_result = vm.resume(None).map_err(|e| anyhow::anyhow!("{}", e)); + } + _ => { + last_result = vm.resume(None).map_err(|e| anyhow::anyhow!("{}", e)); + } + } + } + _ => match &last_result { + Ok(value) => return Ok(Ok(value.clone())), + Err(err) => return Ok(Err(anyhow::anyhow!("{}", err))), + }, + } + } + }; + + let compare_results = |baseline_name: &str, + baseline: &Result, + other_name: &str, + other: &Result| + -> Result<()> { + match (baseline, other) { + (Ok(expected), Ok(actual)) => { + if expected != actual { + return Err(anyhow::anyhow!( + "{} execution result {:?} differed from {} {:?}", + other_name, + actual, + baseline_name, + expected + )); + } + Ok(()) + } + (Err(expected_err), Err(other_err)) => { + let expected_msg = expected_err.to_string(); + let other_msg = other_err.to_string(); + if expected_msg != other_msg { + return Err(anyhow::anyhow!( + "{} execution error '{}' differed from {} '{}'", + other_name, + other_msg, + baseline_name, + expected_msg + )); + } + Ok(()) + } + (Ok(expected), Err(other_err)) => Err(anyhow::anyhow!( + "{} execution failed with '{}' while {} succeeded with {:?}", + other_name, + other_err, + baseline_name, + expected + )), + (Err(expected_err), Ok(actual)) => Err(anyhow::anyhow!( + "{} execution succeeded with {:?} while {} failed with '{}'", + other_name, + actual, + baseline_name, + expected_err + )), + } + }; + + let run_to_completion = run_with_mode( + ExecutionMode::RunToCompletion, + false, + processed_host_responses_run_to_completion.clone(), + )?; + let suspendable = run_with_mode( + ExecutionMode::Suspendable, + false, + processed_host_responses_suspendable.clone(), + )?; + let stepwise = run_with_mode( + ExecutionMode::Suspendable, + true, + processed_host_responses_suspendable.clone(), + )?; + + const HOST_AWAIT_RESPONSE_MISSING: &str = "HostAwait executed but no response provided"; + + let ignore_run_to_completion = ignore_run_to_completion_hostawait_failure + && matches!( + &run_to_completion, + Err(err) if err.to_string().contains(HOST_AWAIT_RESPONSE_MISSING) + ); + + if ignore_run_to_completion { + compare_results("suspendable", &suspendable, "step-by-step", &stepwise)?; + return suspendable; + } + + compare_results( + "run-to-completion", + &run_to_completion, + "suspendable", + &suspendable, + )?; + compare_results( + "run-to-completion", + &run_to_completion, + "step-by-step", + &stepwise, + )?; + + run_to_completion + } + + fn run_vm_test_suite(file: &str) -> Result<()> { + std::println!("Running VM test suite: {}", file); + let yaml_content = fs::read_to_string(file)?; + let test_suite: VmTestSuite = serde_yaml::from_str(&yaml_content)?; + + for test_case in test_suite.cases { + std::println!("Running VM test case: {}", test_case.note); + + let instructions = test_case + .instructions + .iter() + .map(|instruction_str| parse_instruction(instruction_str)) + .collect::>>()?; + + let ignore_hostawait_failure = test_case.ignore_run_to_completion_hostawait_failure; + + struct ModeExpectation<'a> { + strict: bool, + want_result: Option<&'a crate::Value>, + want_error: Option<&'a String>, + } + + let mut expectations = Vec::new(); + + if test_case.want_result.is_some() || test_case.want_error.is_some() { + expectations.push(ModeExpectation { + strict: false, + want_result: test_case.want_result.as_ref(), + want_error: test_case.want_error.as_ref(), + }); + } + + if test_case.want_result_strict.is_some() || test_case.want_error_strict.is_some() { + expectations.push(ModeExpectation { + strict: true, + want_result: test_case.want_result_strict.as_ref(), + want_error: test_case.want_error_strict.as_ref(), + }); + } + + if expectations.is_empty() { + panic!( + "Test case '{}' must specify expectations for at least one mode", + test_case.note + ); + } + + for expectation in expectations { + let mode_label = if expectation.strict { + "strict" + } else { + "non-strict" + }; + std::println!(" Mode: {}", mode_label); + + let execution_result = execute_vm_instructions( + instructions.clone(), + test_case.literals.clone(), + test_case.rule_infos.clone(), + test_case.rule_tree.clone(), + test_case.instruction_params.clone(), + test_case.data.clone(), + test_case.input.clone(), + test_case.max_instructions, + test_case.host_await_responses.clone(), + test_case.host_await_responses_run_to_completion.clone(), + test_case.host_await_responses_suspendable.clone(), + ignore_hostawait_failure, + expectation.strict, + ); + + if expectation.want_error.is_some() && expectation.want_result.is_some() { + panic!( + "Test case '{}' cannot specify both want_result and want_error for {} mode", + test_case.note, mode_label + ); + } + + if let Some(expected_error) = expectation.want_error { + match execution_result { + Err(e) => { + let error_msg = std::format!("{}", e); + if !error_msg.contains(expected_error) { + std::println!( + "Test case '{}' failed ({} mode):", + test_case.note, + mode_label + ); + std::println!(" Expected error containing: '{}'", expected_error); + std::println!(" Actual error: '{}'", error_msg); + panic!("VM test case failed: {}", test_case.note); + } + } + Ok(result) => { + std::println!( + "Test case '{}' failed ({} mode):", + test_case.note, + mode_label + ); + std::println!(" Expected error containing: '{}'", expected_error); + std::println!(" But got successful result: {:?}", result); + panic!("VM test case failed: {}", test_case.note); + } + } + } else if let Some(want_result) = expectation.want_result { + let expected_result = process_value(want_result)?; + + let actual_result = match execution_result { + Ok(result) => result, + Err(e) => { + if std::format!("{}", e).contains("Assertion failed") { + Value::Undefined + } else { + return Err(e); + } + } + }; + + if actual_result != expected_result { + std::println!( + "Test case '{}' failed ({} mode):", + test_case.note, + mode_label + ); + std::println!(" Expected: {:?}", expected_result); + std::println!(" Actual: {:?}", actual_result); + panic!("VM test case failed: {}", test_case.note); + } + } else { + panic!( + "Test case '{}' must specify either want_result or want_error for {} mode", + test_case.note, mode_label + ); + } + + std::println!(" ✓ {} mode passed", mode_label); + } + + std::println!("✓ Test case '{}' passed", test_case.note); + } + std::println!("✓ Test suite '{}' completed successfully", file); + + Ok(()) + } + + #[test_resources("tests/rvm/vm/suites/*.yaml")] + fn run_vm_test_file(file: &str) { + run_vm_test_suite(file).unwrap() + } + + #[test_resources("tests/rvm/vm/suites/loops/*.yaml")] + fn run_loop_test_file(file: &str) { + run_vm_test_suite(file).unwrap() + } +} diff --git a/src/rvm/vm/arithmetic.rs b/src/rvm/vm/arithmetic.rs new file mode 100644 index 0000000..fab49b7 --- /dev/null +++ b/src/rvm/vm/arithmetic.rs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::number::Number; +use crate::value::Value; + +use super::errors::{Result, VmError}; +use super::machine::RegoVM; + +impl RegoVM { + /// Add two values using interpreter's arithmetic logic + pub(super) fn add_values(&self, a: &Value, b: &Value) -> Result { + match (a, b) { + (Value::Number(x), Value::Number(y)) => Ok(Value::from(x.add(y)?)), + _ => Err(VmError::InvalidAddition { + left: a.clone(), + right: b.clone(), + }), + } + } + + /// Subtract two values using interpreter's arithmetic logic + pub(super) fn sub_values(&self, a: &Value, b: &Value) -> Result { + match (a, b) { + (Value::Number(x), Value::Number(y)) => Ok(Value::from(x.sub(y)?)), + _ => Err(VmError::InvalidSubtraction { + left: a.clone(), + right: b.clone(), + }), + } + } + + /// Multiply two values using interpreter's arithmetic logic + pub(super) fn mul_values(&self, a: &Value, b: &Value) -> Result { + match (a, b) { + (Value::Number(x), Value::Number(y)) => Ok(Value::from(x.mul(y)?)), + _ => Err(VmError::InvalidMultiplication { + left: a.clone(), + right: b.clone(), + }), + } + } + + /// Divide two values using interpreter's arithmetic logic + pub(super) fn div_values(&self, a: &Value, b: &Value) -> Result { + match (a, b) { + (Value::Number(x), Value::Number(y)) => { + if *y == Number::from(0u64) { + if self.strict_builtin_errors { + return Err(VmError::InvalidDivision { + left: a.clone(), + right: b.clone(), + }); + } + return Ok(Value::Undefined); + } + + Ok(Value::from(x.clone().divide(y)?)) + } + _ => Err(VmError::InvalidDivision { + left: a.clone(), + right: b.clone(), + }), + } + } + + /// Modulo two values using interpreter's arithmetic logic + pub(super) fn mod_values(&self, a: &Value, b: &Value) -> Result { + match (a, b) { + (Value::Number(x), Value::Number(y)) => { + if *y == Number::from(0u64) { + if self.strict_builtin_errors { + return Err(VmError::InvalidModulo { + left: a.clone(), + right: b.clone(), + }); + } + return Ok(Value::Undefined); + } + + if !x.is_integer() || !y.is_integer() { + return Err(VmError::ModuloOnFloat); + } + + Ok(Value::from(x.clone().modulo(y)?)) + } + _ => Err(VmError::InvalidModulo { + left: a.clone(), + right: b.clone(), + }), + } + } + + pub(super) fn to_bool(&self, value: &Value) -> Option { + match value { + Value::Bool(b) => Some(*b), + Value::Null if !self.strict_builtin_errors => Some(true), + _ => None, + } + } +} diff --git a/src/rvm/vm/comprehension.rs b/src/rvm/vm/comprehension.rs new file mode 100644 index 0000000..6358c0c --- /dev/null +++ b/src/rvm/vm/comprehension.rs @@ -0,0 +1,543 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::rvm::instructions::{ComprehensionBeginParams, ComprehensionMode}; +use crate::value::Value; +use crate::Rc; +use alloc::collections::BTreeMap; +use alloc::format; +use alloc::sync::Arc; +use alloc::vec::Vec; + +use super::context::{ComprehensionContext, IterationState}; +use super::errors::{Result, VmError}; +use super::execution_model::{ExecutionFrame, ExecutionMode, FrameKind}; +use super::machine::RegoVM; + +impl RegoVM { + pub(super) fn execute_comprehension_begin( + &mut self, + params: &ComprehensionBeginParams, + ) -> Result<()> { + match self.execution_mode { + ExecutionMode::RunToCompletion => { + self.execute_comprehension_begin_run_to_completion(params) + } + ExecutionMode::Suspendable => self.execute_comprehension_begin_suspendable(params), + } + } + + fn execute_comprehension_begin_run_to_completion( + &mut self, + params: &ComprehensionBeginParams, + ) -> Result<()> { + let initial_result = match params.mode { + ComprehensionMode::Set => Value::new_set(), + ComprehensionMode::Array => Value::new_array(), + ComprehensionMode::Object => Value::Object(Rc::new(BTreeMap::new())), + }; + self.registers[params.result_reg as usize] = initial_result.clone(); + + let auto_iterate = params.collection_reg != params.result_reg; + let iteration_state = if auto_iterate { + let source_value = self.registers[params.collection_reg as usize].clone(); + match source_value { + Value::Array(items) => { + if items.is_empty() { + None + } else { + Some(IterationState::Array { items, index: 0 }) + } + } + Value::Object(obj) => { + if obj.is_empty() { + None + } else { + Some(IterationState::Object { + obj, + current_key: None, + first_iteration: true, + }) + } + } + Value::Set(set) => { + if set.is_empty() { + None + } else { + Some(IterationState::Set { + items: set, + current_item: None, + first_iteration: true, + }) + } + } + Value::Undefined => None, + Value::Null => None, + _ => None, + } + } else { + None + }; + + let mut has_iteration = false; + if let Some(state) = iteration_state.as_ref() { + has_iteration = self.setup_next_iteration(state, params.key_reg, params.value_reg)?; + } + + let resume_pc = if auto_iterate { + params.comprehension_end as usize + } else { + params.comprehension_end.saturating_sub(1) as usize + }; + + let mut comprehension_context = ComprehensionContext { + mode: params.mode.clone(), + result_reg: params.result_reg, + key_reg: params.key_reg, + value_reg: params.value_reg, + body_start: params.body_start, + comprehension_end: params.comprehension_end, + iteration_state, + resume_pc, + }; + + if auto_iterate { + if has_iteration { + self.pc = params.body_start as usize - 1; + } else { + comprehension_context.iteration_state = None; + self.pc = params.comprehension_end as usize - 1; + } + } + + self.comprehension_stack.push(comprehension_context); + + Ok(()) + } + + fn execute_comprehension_begin_suspendable( + &mut self, + params: &ComprehensionBeginParams, + ) -> Result<()> { + let initial_result = match params.mode { + ComprehensionMode::Set => Value::new_set(), + ComprehensionMode::Array => Value::new_array(), + ComprehensionMode::Object => Value::Object(Rc::new(BTreeMap::new())), + }; + self.registers[params.result_reg as usize] = initial_result.clone(); + + let auto_iterate = params.collection_reg != params.result_reg; + let iteration_state = if auto_iterate { + let source_value = self.registers[params.collection_reg as usize].clone(); + match source_value { + Value::Array(items) => { + if items.is_empty() { + None + } else { + Some(IterationState::Array { items, index: 0 }) + } + } + Value::Object(obj) => { + if obj.is_empty() { + None + } else { + Some(IterationState::Object { + obj, + current_key: None, + first_iteration: true, + }) + } + } + Value::Set(set) => { + if set.is_empty() { + None + } else { + Some(IterationState::Set { + items: set, + current_item: None, + first_iteration: true, + }) + } + } + Value::Undefined => None, + Value::Null => None, + _ => None, + } + } else { + None + }; + + let has_iteration = if let Some(state) = iteration_state.as_ref() { + self.setup_next_iteration(state, params.key_reg, params.value_reg)? + } else { + false + }; + + let resume_pc = if auto_iterate { + params.comprehension_end as usize + } else { + params.comprehension_end.saturating_sub(1) as usize + }; + + let mut comprehension_context = ComprehensionContext { + mode: params.mode.clone(), + result_reg: params.result_reg, + key_reg: params.key_reg, + value_reg: params.value_reg, + body_start: params.body_start, + comprehension_end: params.comprehension_end, + iteration_state, + resume_pc, + }; + + let next_pc = if auto_iterate { + if has_iteration { + params.body_start as usize + } else { + comprehension_context.iteration_state = None; + params.comprehension_end as usize + } + } else { + self.pc + 1 + }; + + let return_pc = comprehension_context.resume_pc; + + let frame = ExecutionFrame::new( + next_pc, + FrameKind::Comprehension { + return_pc, + context: comprehension_context, + }, + ); + self.execution_stack.push(frame); + + Ok(()) + } + + pub(super) fn execute_comprehension_yield( + &mut self, + value_reg: u8, + key_reg: Option, + ) -> Result<()> { + match self.execution_mode { + ExecutionMode::RunToCompletion => { + self.execute_comprehension_yield_run_to_completion(value_reg, key_reg) + } + ExecutionMode::Suspendable => { + self.execute_comprehension_yield_suspendable(value_reg, key_reg) + } + } + } + + fn execute_comprehension_yield_run_to_completion( + &mut self, + value_reg: u8, + key_reg: Option, + ) -> Result<()> { + let mut comprehension_context = if let Some(context) = self.comprehension_stack.pop() { + context + } else { + return Err(VmError::InvalidIteration { + value: Value::String(Arc::from("No active comprehension")), + }); + }; + + let value_to_add = self.registers[value_reg as usize].clone(); + let key_value = if let Some(key_reg) = key_reg { + Some(self.registers[key_reg as usize].clone()) + } else if matches!(comprehension_context.mode, ComprehensionMode::Object) { + Some(self.registers[comprehension_context.key_reg as usize].clone()) + } else { + None + }; + + let result_reg = comprehension_context.result_reg as usize; + let current_result = self.registers[result_reg].clone(); + let mode = comprehension_context.mode.clone(); + + let updated_result = match (mode, current_result) { + (ComprehensionMode::Set, Value::Set(set)) => { + let mut new_set = set.as_ref().clone(); + new_set.insert(value_to_add); + Value::Set(crate::Rc::new(new_set)) + } + (ComprehensionMode::Array, Value::Array(arr)) => { + let mut new_arr = arr.as_ref().to_vec(); + new_arr.push(value_to_add); + Value::Array(crate::Rc::new(new_arr)) + } + (ComprehensionMode::Object, Value::Object(obj)) => { + if let Some(key) = key_value { + let mut new_obj = obj.as_ref().clone(); + new_obj.insert(key, value_to_add); + Value::Object(crate::Rc::new(new_obj)) + } else { + self.comprehension_stack.push(comprehension_context); + return Err(VmError::InvalidIteration { + value: Value::String(Arc::from("Object comprehension requires key")), + }); + } + } + (_mode, other) => { + self.comprehension_stack.push(comprehension_context); + return Err(VmError::InvalidIteration { value: other }); + } + }; + + self.registers[result_reg] = updated_result; + + if let Some(iter_state) = comprehension_context.iteration_state.as_mut() { + match iter_state { + IterationState::Object { current_key, .. } => { + let tracked_key = + if comprehension_context.key_reg != comprehension_context.value_reg { + self.registers[comprehension_context.key_reg as usize].clone() + } else { + self.registers[comprehension_context.value_reg as usize].clone() + }; + *current_key = Some(tracked_key); + } + IterationState::Set { current_item, .. } => { + *current_item = + Some(self.registers[comprehension_context.value_reg as usize].clone()); + } + IterationState::Array { .. } => {} + } + + iter_state.advance(); + let has_next = self.setup_next_iteration( + iter_state, + comprehension_context.key_reg, + comprehension_context.value_reg, + )?; + + if has_next { + self.pc = comprehension_context.body_start as usize - 1; + } else { + comprehension_context.iteration_state = None; + self.pc = comprehension_context.comprehension_end as usize - 1; + } + } + + self.comprehension_stack.push(comprehension_context); + + Ok(()) + } + + fn execute_comprehension_yield_suspendable( + &mut self, + value_reg: u8, + key_reg: Option, + ) -> Result<()> { + let comprehension_index = (0..self.execution_stack.len()) + .rev() + .find(|&idx| { + self.execution_stack + .get(idx) + .is_some_and(|frame| matches!(frame.kind, FrameKind::Comprehension { .. })) + }) + .ok_or(VmError::InvalidIteration { + value: Value::String(Arc::from("No active comprehension")), + })?; + + let (iteration_state_snapshot, key_reg_idx, value_reg_idx, body_start, comprehension_end) = { + let frame = self.execution_stack.get_mut(comprehension_index).ok_or( + VmError::InvalidIteration { + value: Value::String(Arc::from("No active comprehension")), + }, + )?; + + match &mut frame.kind { + FrameKind::Comprehension { context, .. } => { + let value_to_add = self.registers[value_reg as usize].clone(); + let key_value = if let Some(key_reg) = key_reg { + Some(self.registers[key_reg as usize].clone()) + } else if matches!(context.mode, ComprehensionMode::Object) { + Some(self.registers[context.key_reg as usize].clone()) + } else { + None + }; + + let result_reg_idx = context.result_reg as usize; + let current_result = self.registers[result_reg_idx].clone(); + let mode = context.mode.clone(); + + let updated_result = match (mode, current_result) { + (ComprehensionMode::Set, Value::Set(set)) => { + let mut new_set = set.as_ref().clone(); + new_set.insert(value_to_add); + Value::Set(crate::Rc::new(new_set)) + } + (ComprehensionMode::Array, Value::Array(arr)) => { + let mut new_arr = arr.as_ref().to_vec(); + new_arr.push(value_to_add); + Value::Array(crate::Rc::new(new_arr)) + } + (ComprehensionMode::Object, Value::Object(obj)) => { + if let Some(key) = key_value { + let mut new_obj = obj.as_ref().clone(); + new_obj.insert(key, value_to_add); + Value::Object(crate::Rc::new(new_obj)) + } else { + return Err(VmError::InvalidIteration { + value: Value::String(Arc::from( + "Object comprehension requires key", + )), + }); + } + } + (_mode, other) => { + return Err(VmError::InvalidIteration { value: other }); + } + }; + + self.registers[result_reg_idx] = updated_result; + + if let Some(iter_state) = context.iteration_state.as_mut() { + match iter_state { + IterationState::Object { current_key, .. } => { + let tracked_key = if context.key_reg != context.value_reg { + self.registers[context.key_reg as usize].clone() + } else { + self.registers[context.value_reg as usize].clone() + }; + *current_key = Some(tracked_key); + } + IterationState::Set { current_item, .. } => { + *current_item = + Some(self.registers[context.value_reg as usize].clone()); + } + IterationState::Array { .. } => {} + } + + iter_state.advance(); + } + + ( + context.iteration_state.clone(), + context.key_reg, + context.value_reg, + context.body_start, + context.comprehension_end, + ) + } + _ => { + return Err(VmError::InvalidIteration { + value: Value::String(Arc::from("No active comprehension")), + }); + } + } + }; + + if let Some(state) = iteration_state_snapshot.as_ref() { + let has_next = self.setup_next_iteration(state, key_reg_idx, value_reg_idx)?; + + if has_next { + if let Some(frame) = self.execution_stack.get_mut(comprehension_index) { + frame.pc = body_start as usize; + self.frame_pc_overridden = true; + } + } else if let Some(frame) = self.execution_stack.get_mut(comprehension_index) { + if let FrameKind::Comprehension { context, .. } = &mut frame.kind { + context.iteration_state = None; + } + frame.pc = comprehension_end as usize; + self.frame_pc_overridden = true; + } + } + + Ok(()) + } + + pub(super) fn execute_comprehension_end(&mut self) -> Result<()> { + match self.execution_mode { + ExecutionMode::RunToCompletion => self.execute_comprehension_end_run_to_completion(), + ExecutionMode::Suspendable => self.execute_comprehension_end_suspendable(), + } + } + + fn execute_comprehension_end_run_to_completion(&mut self) -> Result<()> { + if let Some(_context) = self.comprehension_stack.pop() { + Ok(()) + } else { + Err(VmError::InvalidIteration { + value: Value::String(Arc::from("No active comprehension context")), + }) + } + } + + fn execute_comprehension_end_suspendable(&mut self) -> Result<()> { + let mut unwound_frames: Vec = Vec::new(); + + loop { + let frame = match self.execution_stack.pop() { + Some(frame) => frame, + None => { + // Restore any frames we already unwound before propagating the error. + while let Some(restored) = unwound_frames.pop() { + self.execution_stack.push(restored); + } + return Err(VmError::InvalidIteration { + value: Value::String(Arc::from("No active comprehension context")), + }); + } + }; + + let ExecutionFrame { + pc: frame_pc, + kind: frame_kind, + } = frame; + + match frame_kind { + FrameKind::Comprehension { + return_pc: _, + context, + } => { + let raw_target = context.resume_pc; + let resume_pc = if raw_target <= self.pc { + self.pc.saturating_add(1) + } else if raw_target == self.pc.saturating_add(1) { + raw_target + } else { + raw_target.saturating_sub(1) + }; + if let Some(parent) = self.execution_stack.last_mut() { + parent.pc = resume_pc; + } + while let Some(restored) = unwound_frames.pop() { + self.execution_stack.push(restored); + } + return Ok(()); + } + FrameKind::Loop { return_pc, context } => { + if let Some(parent) = self.execution_stack.last_mut() { + parent.pc = return_pc; + } + // Keep the loop frame available so we can restore it if we discover a mismatch. + unwound_frames.push(ExecutionFrame::new( + frame_pc, + FrameKind::Loop { return_pc, context }, + )); + } + other_kind => { + let message = format!( + "Mismatched comprehension frame: frame={:?} stack_depth={} unwound_loops={}", + &other_kind, + self.execution_stack.len(), + unwound_frames.len() + ); + // Put the unexpected frame back on the stack along with any loops we unwound. + self.execution_stack + .push(ExecutionFrame::new(frame_pc, other_kind)); + while let Some(restored) = unwound_frames.pop() { + self.execution_stack.push(restored); + } + return Err(VmError::InvalidIteration { + value: Value::String(Arc::from(message.into_boxed_str())), + }); + } + } + } + } +} diff --git a/src/rvm/vm/context.rs b/src/rvm/vm/context.rs new file mode 100644 index 0000000..f03fbec --- /dev/null +++ b/src/rvm/vm/context.rs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::rvm::instructions::{ComprehensionMode, LoopMode}; +use crate::value::Value; +use crate::Rc; +use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::vec::Vec; + +/// Loop execution context for managing iteration state +#[derive(Debug, Clone)] +pub struct LoopContext { + pub mode: LoopMode, + pub iteration_state: IterationState, + pub key_reg: u8, + pub value_reg: u8, + pub result_reg: u8, + pub body_start: u16, + pub loop_end: u16, + pub loop_next_pc: u16, // PC of the LoopNext instruction to avoid searching + pub body_resume_pc: usize, + pub success_count: usize, + pub total_iterations: usize, + pub current_iteration_failed: bool, // Track if current iteration had condition failures +} + +/// Iterator state for different collection types +#[derive(Debug, Clone)] +pub enum IterationState { + Array { + items: Rc>, + index: usize, + }, + Object { + obj: Rc>, + current_key: Option, + first_iteration: bool, + }, + Set { + items: Rc>, + current_item: Option, + first_iteration: bool, + }, +} + +impl IterationState { + pub(super) fn advance(&mut self) { + match self { + IterationState::Array { index, .. } => { + *index += 1; + } + IterationState::Object { + first_iteration, .. + } => { + *first_iteration = false; + } + IterationState::Set { + first_iteration, .. + } => { + *first_iteration = false; + } + } + } +} + +#[allow(unused)] +#[derive(Debug, Clone)] +pub struct CallRuleContext { + pub return_pc: usize, + pub dest_reg: u8, + pub result_reg: u8, + pub rule_index: u16, + pub rule_type: crate::rvm::program::RuleType, + pub current_definition_index: usize, + pub current_body_index: usize, +} + +/// Context for tracking active comprehensions +#[derive(Debug, Clone)] +pub(super) struct ComprehensionContext { + /// Type of comprehension (Array, Set, Object) + pub(super) mode: ComprehensionMode, + /// Register storing the comprehension result collection + pub(super) result_reg: u8, + /// Register holding the current iteration key + pub(super) key_reg: u8, + /// Register holding the current iteration value + pub(super) value_reg: u8, + /// Jump target for comprehension body start + pub(super) body_start: u16, + /// Jump target for comprehension end + pub(super) comprehension_end: u16, + /// Iteration state when comprehension manages iteration itself (None when driven by LoopStart/LoopNext) + pub(super) iteration_state: Option, + /// Resume location for the parent frame once this comprehension completes + pub(super) resume_pc: usize, +} diff --git a/src/rvm/vm/dispatch.rs b/src/rvm/vm/dispatch.rs new file mode 100644 index 0000000..464c05c --- /dev/null +++ b/src/rvm/vm/dispatch.rs @@ -0,0 +1,771 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::rvm::instructions::{Instruction, LiteralOrRegister}; +use crate::rvm::program::Program; +use crate::value::Value; +use alloc::collections::BTreeSet; +use alloc::vec::Vec; +use core::mem; + +use super::errors::{Result, VmError}; +use super::execution_model::{ExecutionMode, SuspendReason}; +use super::loops::LoopParams; +use super::machine::RegoVM; + +pub(super) enum InstructionOutcome { + Continue, + Return(Value), + Break, + Suspend { reason: SuspendReason }, +} + +impl RegoVM { + pub(super) fn execute_instruction( + &mut self, + program: &Program, + instruction: Instruction, + ) -> Result { + self.execute_load_and_move(program, instruction) + } + + fn execute_load_and_move( + &mut self, + program: &Program, + instruction: Instruction, + ) -> Result { + use Instruction::*; + match instruction { + Load { dest, literal_idx } => { + if let Some(value) = program.literals.get(literal_idx as usize) { + self.registers[dest as usize] = value.clone(); + Ok(InstructionOutcome::Continue) + } else { + Err(VmError::LiteralIndexOutOfBounds { + index: literal_idx as usize, + }) + } + } + LoadTrue { dest } => { + self.registers[dest as usize] = Value::Bool(true); + Ok(InstructionOutcome::Continue) + } + LoadFalse { dest } => { + self.registers[dest as usize] = Value::Bool(false); + Ok(InstructionOutcome::Continue) + } + LoadNull { dest } => { + self.registers[dest as usize] = Value::Null; + Ok(InstructionOutcome::Continue) + } + LoadBool { dest, value } => { + self.registers[dest as usize] = Value::Bool(value); + Ok(InstructionOutcome::Continue) + } + LoadData { dest } => { + self.registers[dest as usize] = self.data.clone(); + Ok(InstructionOutcome::Continue) + } + LoadInput { dest } => { + self.registers[dest as usize] = self.input.clone(); + Ok(InstructionOutcome::Continue) + } + Move { dest, src } => { + self.registers[dest as usize] = self.registers[src as usize].clone(); + Ok(InstructionOutcome::Continue) + } + other => self.execute_arithmetic_instruction(program, other), + } + } + + fn execute_arithmetic_instruction( + &mut self, + _program: &Program, + instruction: Instruction, + ) -> Result { + use Instruction::*; + match instruction { + Add { dest, left, right } => { + let a = &self.registers[left as usize]; + let b = &self.registers[right as usize]; + + if a == &Value::Undefined || b == &Value::Undefined { + self.registers[dest as usize] = Value::Undefined; + return Ok(InstructionOutcome::Continue); + } + + let result = self.add_values(a, b)?; + self.registers[dest as usize] = result; + Ok(InstructionOutcome::Continue) + } + Sub { dest, left, right } => { + let a = &self.registers[left as usize]; + let b = &self.registers[right as usize]; + + if a == &Value::Undefined || b == &Value::Undefined { + self.registers[dest as usize] = Value::Undefined; + return Ok(InstructionOutcome::Continue); + } + + let result = self.sub_values(a, b)?; + self.registers[dest as usize] = result; + Ok(InstructionOutcome::Continue) + } + Mul { dest, left, right } => { + let a = &self.registers[left as usize]; + let b = &self.registers[right as usize]; + + if a == &Value::Undefined || b == &Value::Undefined { + self.registers[dest as usize] = Value::Undefined; + return Ok(InstructionOutcome::Continue); + } + + let result = self.mul_values(a, b)?; + self.registers[dest as usize] = result; + Ok(InstructionOutcome::Continue) + } + Div { dest, left, right } => { + let a = &self.registers[left as usize]; + let b = &self.registers[right as usize]; + + if a == &Value::Undefined || b == &Value::Undefined { + self.registers[dest as usize] = Value::Undefined; + return Ok(InstructionOutcome::Continue); + } + + let result = self.div_values(a, b)?; + self.registers[dest as usize] = result; + Ok(InstructionOutcome::Continue) + } + Mod { dest, left, right } => { + let a = &self.registers[left as usize]; + let b = &self.registers[right as usize]; + + if a == &Value::Undefined || b == &Value::Undefined { + self.registers[dest as usize] = Value::Undefined; + return Ok(InstructionOutcome::Continue); + } + + let result = self.mod_values(a, b)?; + self.registers[dest as usize] = result; + Ok(InstructionOutcome::Continue) + } + other => self.execute_comparison_instruction(_program, other), + } + } + + fn execute_comparison_instruction( + &mut self, + _program: &Program, + instruction: Instruction, + ) -> Result { + use Instruction::*; + match instruction { + Eq { dest, left, right } => { + let a = &self.registers[left as usize]; + let b = &self.registers[right as usize]; + + if a == &Value::Undefined || b == &Value::Undefined { + self.registers[dest as usize] = Value::Undefined; + return Ok(InstructionOutcome::Continue); + } + + self.registers[dest as usize] = Value::Bool(a == b); + Ok(InstructionOutcome::Continue) + } + Ne { dest, left, right } => { + let a = &self.registers[left as usize]; + let b = &self.registers[right as usize]; + + if a == &Value::Undefined || b == &Value::Undefined { + self.registers[dest as usize] = Value::Undefined; + return Ok(InstructionOutcome::Continue); + } + + self.registers[dest as usize] = Value::Bool(a != b); + Ok(InstructionOutcome::Continue) + } + Lt { dest, left, right } => { + let a = &self.registers[left as usize]; + let b = &self.registers[right as usize]; + + if a == &Value::Undefined || b == &Value::Undefined { + self.registers[dest as usize] = Value::Undefined; + return Ok(InstructionOutcome::Continue); + } + + if self.strict_builtin_errors && mem::discriminant(a) != mem::discriminant(b) { + return Err(VmError::ArithmeticError(alloc::format!( + "#undefined: cannot compare values of different types (left={a:?}, right={b:?})" + ))); + } + + self.registers[dest as usize] = Value::Bool(a < b); + Ok(InstructionOutcome::Continue) + } + Le { dest, left, right } => { + let a = &self.registers[left as usize]; + let b = &self.registers[right as usize]; + + if a == &Value::Undefined || b == &Value::Undefined { + self.registers[dest as usize] = Value::Undefined; + return Ok(InstructionOutcome::Continue); + } + + if self.strict_builtin_errors && mem::discriminant(a) != mem::discriminant(b) { + return Err(VmError::ArithmeticError(alloc::format!( + "#undefined: cannot compare values of different types (left={a:?}, right={b:?})" + ))); + } + + self.registers[dest as usize] = Value::Bool(a <= b); + Ok(InstructionOutcome::Continue) + } + Gt { dest, left, right } => { + let a = &self.registers[left as usize]; + let b = &self.registers[right as usize]; + + if a == &Value::Undefined || b == &Value::Undefined { + self.registers[dest as usize] = Value::Undefined; + return Ok(InstructionOutcome::Continue); + } + + if self.strict_builtin_errors && mem::discriminant(a) != mem::discriminant(b) { + return Err(VmError::ArithmeticError(alloc::format!( + "#undefined: cannot compare values of different types (left={a:?}, right={b:?})" + ))); + } + + self.registers[dest as usize] = Value::Bool(a > b); + Ok(InstructionOutcome::Continue) + } + Ge { dest, left, right } => { + let a = &self.registers[left as usize]; + let b = &self.registers[right as usize]; + + if a == &Value::Undefined || b == &Value::Undefined { + self.registers[dest as usize] = Value::Undefined; + return Ok(InstructionOutcome::Continue); + } + + if self.strict_builtin_errors && mem::discriminant(a) != mem::discriminant(b) { + return Err(VmError::ArithmeticError(alloc::format!( + "#undefined: cannot compare values of different types (left={a:?}, right={b:?})" + ))); + } + + self.registers[dest as usize] = Value::Bool(a >= b); + Ok(InstructionOutcome::Continue) + } + And { dest, left, right } => { + let left_value = &self.registers[left as usize]; + let right_value = &self.registers[right as usize]; + + if left_value == &Value::Undefined || right_value == &Value::Undefined { + self.registers[dest as usize] = Value::Undefined; + return Ok(InstructionOutcome::Continue); + } + + match (self.to_bool(left_value), self.to_bool(right_value)) { + (Some(a), Some(b)) => { + self.registers[dest as usize] = Value::Bool(a && b); + Ok(InstructionOutcome::Continue) + } + _ => Err(VmError::ArithmeticError(alloc::format!( + "#undefined: logical AND expects booleans (left={left_value:?}, right={right_value:?})" + ))), + } + } + Or { dest, left, right } => { + let left_value = &self.registers[left as usize]; + let right_value = &self.registers[right as usize]; + + if left_value == &Value::Undefined || right_value == &Value::Undefined { + self.registers[dest as usize] = Value::Undefined; + return Ok(InstructionOutcome::Continue); + } + + match (self.to_bool(left_value), self.to_bool(right_value)) { + (Some(a), Some(b)) => { + self.registers[dest as usize] = Value::Bool(a || b); + Ok(InstructionOutcome::Continue) + } + _ => Err(VmError::ArithmeticError(alloc::format!( + "#undefined: logical OR expects booleans (left={left_value:?}, right={right_value:?})" + ))), + } + } + Not { dest, operand } => { + let operand_value = &self.registers[operand as usize]; + + if operand_value == &Value::Undefined { + self.registers[dest as usize] = Value::Undefined; + return Ok(InstructionOutcome::Continue); + } + + if let Some(value) = self.to_bool(operand_value) { + self.registers[dest as usize] = Value::Bool(!value); + Ok(InstructionOutcome::Continue) + } else { + Err(VmError::ArithmeticError(alloc::format!( + "#undefined: logical NOT expects a boolean (operand={operand_value:?})" + ))) + } + } + AssertCondition { condition } => { + let value = &self.registers[condition as usize]; + + let condition_result = match value { + Value::Bool(b) => *b, + Value::Undefined => false, + _ => true, + }; + + self.handle_condition(condition_result)?; + Ok(InstructionOutcome::Continue) + } + AssertNotUndefined { register } => { + let value = &self.registers[register as usize]; + + let is_undefined = matches!(value, Value::Undefined); + self.handle_condition(!is_undefined)?; + Ok(InstructionOutcome::Continue) + } + other => self.execute_call_instruction(_program, other), + } + } + + fn execute_call_instruction( + &mut self, + _program: &Program, + instruction: Instruction, + ) -> Result { + use Instruction::*; + match instruction { + BuiltinCall { params_index } => { + self.execute_builtin_call(params_index)?; + Ok(InstructionOutcome::Continue) + } + HostAwait { dest, arg, id } => { + let argument = self.registers[arg as usize].clone(); + let identifier = self + .registers + .get(id as usize) + .cloned() + .unwrap_or(Value::Undefined); + match self.execution_mode { + ExecutionMode::RunToCompletion => { + let response = self.next_host_await_response(&identifier, dest)?; + if self.registers.len() <= dest as usize { + self.registers.resize(dest as usize + 1, Value::Undefined); + } + self.registers[dest as usize] = response; + Ok(InstructionOutcome::Continue) + } + ExecutionMode::Suspendable => Ok(InstructionOutcome::Suspend { + reason: SuspendReason::HostAwait { + dest, + argument, + identifier, + }, + }), + } + } + FunctionCall { params_index } => { + self.execute_function_call(params_index)?; + Ok(InstructionOutcome::Continue) + } + Return { value } => { + let result = self.registers[value as usize].clone(); + Ok(InstructionOutcome::Return(result)) + } + CallRule { dest, rule_index } => { + self.execute_call_rule(dest, rule_index)?; + Ok(InstructionOutcome::Continue) + } + RuleInit { + result_reg, + rule_index, + } => { + self.execute_rule_init(result_reg, rule_index)?; + Ok(InstructionOutcome::Continue) + } + DestructuringSuccess {} => Ok(InstructionOutcome::Break), + RuleReturn {} => { + self.execute_rule_return()?; + Ok(InstructionOutcome::Break) + } + other => self.execute_collection_instruction(_program, other), + } + } + + fn execute_collection_instruction( + &mut self, + program: &Program, + instruction: Instruction, + ) -> Result { + use Instruction::*; + match instruction { + ObjectSet { obj, key, value } => { + let key_value = self.registers[key as usize].clone(); + let value_value = self.registers[value as usize].clone(); + + let mut obj_value = mem::replace(&mut self.registers[obj as usize], Value::Null); + + if let Ok(obj_mut) = obj_value.as_object_mut() { + obj_mut.insert(key_value, value_value); + self.registers[obj as usize] = obj_value; + } else { + self.registers[obj as usize] = obj_value; + return Err(VmError::RegisterNotObject { register: obj }); + } + Ok(InstructionOutcome::Continue) + } + ObjectCreate { params_index } => { + let params = program + .instruction_data + .get_object_create_params(params_index) + .ok_or(VmError::InvalidObjectCreateParams { + index: params_index, + })?; + + let mut any_undefined = false; + + for &(_, value_reg) in params.literal_key_field_pairs() { + if matches!(self.registers[value_reg as usize], Value::Undefined) { + any_undefined = true; + break; + } + } + + if !any_undefined { + for &(key_reg, value_reg) in params.field_pairs() { + if matches!(self.registers[key_reg as usize], Value::Undefined) + || matches!(self.registers[value_reg as usize], Value::Undefined) + { + any_undefined = true; + break; + } + } + } + + if any_undefined { + self.registers[params.dest as usize] = Value::Undefined; + } else { + let mut obj_value = program + .literals + .get(params.template_literal_idx as usize) + .ok_or(VmError::InvalidTemplateLiteralIndex { + index: params.template_literal_idx, + })? + .clone(); + + if let Ok(obj_mut) = obj_value.as_object_mut() { + let mut literal_updates = params.literal_key_field_pairs().iter(); + let mut current_literal_update = literal_updates.next(); + + for (key, value) in obj_mut.iter_mut() { + if let Some(&(literal_idx, value_reg)) = current_literal_update { + if let Some(literal_key) = + program.literals.get(literal_idx as usize) + { + if key == literal_key { + *value = self.registers[value_reg as usize].clone(); + current_literal_update = literal_updates.next(); + } + } + } else { + break; + } + } + + while let Some(&(literal_idx, value_reg)) = current_literal_update { + if let Some(key_value) = program.literals.get(literal_idx as usize) { + let value_value = self.registers[value_reg as usize].clone(); + obj_mut.insert(key_value.clone(), value_value); + } + current_literal_update = literal_updates.next(); + } + + for &(key_reg, value_reg) in params.field_pairs() { + let key_value = self.registers[key_reg as usize].clone(); + let value_value = self.registers[value_reg as usize].clone(); + obj_mut.insert(key_value, value_value); + } + } else { + return Err(VmError::ObjectCreateInvalidTemplate); + } + + self.registers[params.dest as usize] = obj_value; + } + Ok(InstructionOutcome::Continue) + } + Index { + dest, + container, + key, + } => { + let key_value = &self.registers[key as usize]; + let container_value = &self.registers[container as usize]; + let result = container_value[key_value].clone(); + self.registers[dest as usize] = result; + Ok(InstructionOutcome::Continue) + } + IndexLiteral { + dest, + container, + literal_idx, + } => { + let container_value = &self.registers[container as usize]; + + if let Some(key_value) = program.literals.get(literal_idx as usize) { + let result = container_value[key_value].clone(); + self.registers[dest as usize] = result; + Ok(InstructionOutcome::Continue) + } else { + Err(VmError::LiteralIndexOutOfBounds { + index: literal_idx as usize, + }) + } + } + ArrayNew { dest } => { + let empty_array = Value::Array(crate::Rc::new(Vec::new())); + self.registers[dest as usize] = empty_array; + Ok(InstructionOutcome::Continue) + } + ArrayPush { arr, value } => { + let value_to_push = self.registers[value as usize].clone(); + + let mut arr_value = mem::replace(&mut self.registers[arr as usize], Value::Null); + + if let Ok(arr_mut) = arr_value.as_array_mut() { + arr_mut.push(value_to_push); + self.registers[arr as usize] = arr_value; + } else { + self.registers[arr as usize] = arr_value; + return Err(VmError::RegisterNotArray { register: arr }); + } + Ok(InstructionOutcome::Continue) + } + ArrayCreate { params_index } => { + if let Some(params) = program + .instruction_data + .get_array_create_params(params_index) + { + let mut any_undefined = false; + for ® in params.element_registers() { + if matches!(self.registers[reg as usize], Value::Undefined) { + any_undefined = true; + break; + } + } + + if any_undefined { + self.registers[params.dest as usize] = Value::Undefined; + } else { + let elements: Vec = params + .element_registers() + .iter() + .map(|®| self.registers[reg as usize].clone()) + .collect(); + + let array_value = Value::Array(crate::Rc::new(elements)); + self.registers[params.dest as usize] = array_value; + } + Ok(InstructionOutcome::Continue) + } else { + Err(VmError::InvalidArrayCreateParams { + index: params_index, + }) + } + } + SetNew { dest } => { + let empty_set = Value::Set(crate::Rc::new(BTreeSet::new())); + self.registers[dest as usize] = empty_set; + Ok(InstructionOutcome::Continue) + } + SetAdd { set, value } => { + let value_to_add = self.registers[value as usize].clone(); + + let mut set_value = mem::replace(&mut self.registers[set as usize], Value::Null); + + if let Ok(set_mut) = set_value.as_set_mut() { + set_mut.insert(value_to_add); + self.registers[set as usize] = set_value; + } else { + self.registers[set as usize] = set_value; + return Err(VmError::RegisterNotSet { register: set }); + } + Ok(InstructionOutcome::Continue) + } + SetCreate { params_index } => { + if let Some(params) = program.instruction_data.get_set_create_params(params_index) { + let mut any_undefined = false; + for ® in params.element_registers() { + if matches!(self.registers[reg as usize], Value::Undefined) { + any_undefined = true; + break; + } + } + + if any_undefined { + self.registers[params.dest as usize] = Value::Undefined; + } else { + let mut set = BTreeSet::new(); + for ® in params.element_registers() { + set.insert(self.registers[reg as usize].clone()); + } + + let set_value = Value::Set(crate::Rc::new(set)); + self.registers[params.dest as usize] = set_value; + } + Ok(InstructionOutcome::Continue) + } else { + Err(VmError::InvalidSetCreateParams { + index: params_index, + }) + } + } + Contains { + dest, + collection, + value, + } => { + let value_to_check = &self.registers[value as usize]; + let collection_value = &self.registers[collection as usize]; + + let result = match collection_value { + Value::Set(set_elements) => Value::Bool(set_elements.contains(value_to_check)), + Value::Array(array_items) => Value::Bool(array_items.contains(value_to_check)), + Value::Object(object_fields) => Value::Bool( + object_fields.contains_key(value_to_check) + || object_fields.values().any(|v| v == value_to_check), + ), + _ => Value::Bool(false), + }; + + self.registers[dest as usize] = result; + Ok(InstructionOutcome::Continue) + } + Count { dest, collection } => { + let collection_value = &self.registers[collection as usize]; + + let result = match collection_value { + Value::Array(array_items) => Value::from(array_items.len()), + Value::Object(object_fields) => Value::from(object_fields.len()), + Value::Set(set_elements) => Value::from(set_elements.len()), + _ => Value::Undefined, + }; + + self.registers[dest as usize] = result; + Ok(InstructionOutcome::Continue) + } + other => self.execute_loop_instruction(program, other), + } + } + + fn execute_loop_instruction( + &mut self, + program: &Program, + instruction: Instruction, + ) -> Result { + use Instruction::*; + match instruction { + LoopStart { params_index } => { + let loop_params = &self.program.instruction_data.loop_params[params_index as usize]; + let mode = loop_params.mode.clone(); + let params = LoopParams { + collection: loop_params.collection, + key_reg: loop_params.key_reg, + value_reg: loop_params.value_reg, + result_reg: loop_params.result_reg, + body_start: loop_params.body_start, + loop_end: loop_params.loop_end, + }; + self.execute_loop_start(&mode, params)?; + Ok(InstructionOutcome::Continue) + } + LoopNext { + body_start, + loop_end, + } => { + self.execute_loop_next(body_start, loop_end)?; + Ok(InstructionOutcome::Continue) + } + Halt {} => { + let result = self.registers[0].clone(); + Ok(InstructionOutcome::Return(result)) + } + other => self.execute_virtual_instruction(program, other), + } + } + + fn execute_virtual_instruction( + &mut self, + program: &Program, + instruction: Instruction, + ) -> Result { + use Instruction::*; + match instruction { + ChainedIndex { params_index } => { + let params = program + .instruction_data + .get_chained_index_params(params_index) + .ok_or(VmError::InvalidChainedIndexParams { + index: params_index, + })?; + + let mut current_value = self.registers[params.root as usize].clone(); + + for component in ¶ms.path_components { + let key_value = match component { + LiteralOrRegister::Literal(idx) => program + .literals + .get(*idx as usize) + .ok_or(VmError::LiteralIndexOutOfBounds { + index: *idx as usize, + })? + .clone(), + LiteralOrRegister::Register(reg) => self.registers[*reg as usize].clone(), + }; + + current_value = current_value[&key_value].clone(); + + if current_value == Value::Undefined { + break; + } + } + + self.registers[params.dest as usize] = current_value; + Ok(InstructionOutcome::Continue) + } + VirtualDataDocumentLookup { params_index } => { + self.execute_virtual_data_document_lookup(params_index)?; + Ok(InstructionOutcome::Continue) + } + ComprehensionBegin { params_index } => { + let params = program + .instruction_data + .get_comprehension_begin_params(params_index) + .ok_or(VmError::InvalidComprehensionBeginParams { + index: params_index, + })? + .clone(); + self.execute_comprehension_begin(¶ms)?; + Ok(InstructionOutcome::Continue) + } + ComprehensionYield { value_reg, key_reg } => { + self.execute_comprehension_yield(value_reg, key_reg)?; + Ok(InstructionOutcome::Continue) + } + ComprehensionEnd {} => { + self.execute_comprehension_end()?; + Ok(InstructionOutcome::Continue) + } + unexpected => Err(VmError::Internal(alloc::format!( + "Unhandled instruction variant: {:?}", + unexpected + ))), + } + } +} diff --git a/src/rvm/vm/errors.rs b/src/rvm/vm/errors.rs new file mode 100644 index 0000000..4c2d3bd --- /dev/null +++ b/src/rvm/vm/errors.rs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::value::Value; +use alloc::string::String; +use alloc::vec::Vec; +use thiserror::Error; + +/// VM execution errors +#[derive(Error, Debug, Clone, PartialEq)] +pub enum VmError { + #[error("Execution stopped: exceeded maximum instruction limit of {limit}")] + InstructionLimitExceeded { limit: usize }, + + #[error("Literal index {index} out of bounds")] + LiteralIndexOutOfBounds { index: usize }, + + #[error("Register {register} does not contain an object")] + RegisterNotObject { register: u8 }, + + #[error("ObjectCreate: template is not an object")] + ObjectCreateInvalidTemplate, + + #[error("Register {register} does not contain an array")] + RegisterNotArray { register: u8 }, + + #[error("Register {register} does not contain a set")] + RegisterNotSet { register: u8 }, + + #[error("Rule index {index} out of bounds")] + RuleIndexOutOfBounds { index: u16 }, + + #[error("Rule index {index} has no info")] + RuleInfoMissing { index: u16 }, + + #[error("Invalid object create params index: {index}")] + InvalidObjectCreateParams { index: u16 }, + + #[error("Invalid template literal index: {index}")] + InvalidTemplateLiteralIndex { index: u16 }, + + #[error("Invalid chained index params index: {index}")] + InvalidChainedIndexParams { index: u16 }, + + #[error("Invalid array create params index: {index}")] + InvalidArrayCreateParams { index: u16 }, + + #[error("Invalid set create params index: {index}")] + InvalidSetCreateParams { index: u16 }, + + #[error("Invalid virtual data document lookup params index: {index}")] + InvalidVirtualDataDocumentLookupParams { index: u16 }, + + #[error("Invalid comprehension start params index: {index}")] + InvalidComprehensionBeginParams { index: u16 }, + + #[error("Invalid rule index: {rule_index:?}")] + InvalidRuleIndex { rule_index: Value }, + + #[error("Invalid rule tree entry: {value:?}")] + InvalidRuleTreeEntry { value: Value }, + + #[error("Builtin function expects exactly {expected} arguments, got {actual}")] + BuiltinArgumentMismatch { expected: u16, actual: usize }, + + #[error("Builtin function not resolved: {name}")] + BuiltinNotResolved { name: String }, + + #[error("Cannot add {left:?} and {right:?}")] + InvalidAddition { left: Value, right: Value }, + + #[error("Cannot subtract {left:?} and {right:?}")] + InvalidSubtraction { left: Value, right: Value }, + + #[error("Cannot multiply {left:?} and {right:?}")] + InvalidMultiplication { left: Value, right: Value }, + + #[error("Cannot divide {left:?} and {right:?}")] + InvalidDivision { left: Value, right: Value }, + + #[error("modulo on floating-point number")] + ModuloOnFloat, + + #[error("Cannot modulo {left:?} and {right:?}")] + InvalidModulo { left: Value, right: Value }, + + #[error("Cannot iterate over {value:?}")] + InvalidIteration { value: Value }, + + #[error("HostAwait executed but no response provided for destination register {dest} (id: {identifier:?})")] + HostAwaitResponseMissing { dest: u8, identifier: Value }, + + #[error("Assertion failed")] + AssertionFailed, + + #[error("Rule-data conflict: {0}")] + RuleDataConflict(String), + + #[error("Arithmetic error: {0}")] + ArithmeticError(String), + + #[error("Entry point index {index} out of bounds (max: {max_index})")] + InvalidEntryPointIndex { index: usize, max_index: usize }, + + #[error("Entry point '{name}' not found. Available entry points: {available:?}")] + EntryPointNotFound { + name: String, + available: Vec, + }, + + #[error("Internal VM error: {0}")] + Internal(String), +} + +impl From for VmError { + fn from(err: anyhow::Error) -> Self { + VmError::ArithmeticError(alloc::format!("{}", err)) + } +} + +pub type Result = core::result::Result; diff --git a/src/rvm/vm/execution.rs b/src/rvm/vm/execution.rs new file mode 100644 index 0000000..d6b95eb --- /dev/null +++ b/src/rvm/vm/execution.rs @@ -0,0 +1,591 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +use crate::rvm::instructions::Instruction; +use crate::rvm::program::Program; +use crate::value::Value; +use alloc::string::String; +use alloc::vec::Vec; + +use super::dispatch::InstructionOutcome; +use super::errors::{Result, VmError}; +use super::execution_model::{ + ExecutionFrame, ExecutionMode, ExecutionState, FrameKind, RuleFrameData, RuleFramePhase, + SuspendReason, +}; +use super::machine::RegoVM; + +impl RegoVM { + pub fn execute(&mut self) -> Result { + match self.execution_mode { + ExecutionMode::RunToCompletion => self.execute_run_to_completion(), + ExecutionMode::Suspendable => self.execute_suspendable(), + } + } + + pub fn execute_entry_point_by_index(&mut self, index: usize) -> Result { + let entry_points: Vec<(String, usize)> = self + .program + .entry_points + .iter() + .map(|(name, pc)| (name.clone(), *pc)) + .collect(); + + if index >= entry_points.len() { + return Err(VmError::InvalidEntryPointIndex { + index, + max_index: entry_points.len().saturating_sub(1), + }); + } + + let (_entry_point_name, entry_point_pc) = &entry_points[index]; + + if *entry_point_pc >= self.program.instructions.len() { + return Err(VmError::Internal(alloc::format!( + "Entry point PC {} >= instruction count {} for index {} | {}", + entry_point_pc, + self.program.instructions.len(), + index, + self.get_debug_state() + ))); + } + + match self.execution_mode { + ExecutionMode::RunToCompletion => { + self.reset_execution_state(); + + if let Err(e) = self.validate_vm_state() { + return Err(VmError::Internal(alloc::format!( + "VM state validation failed before entry point execution: {} | {}", + e, + self.get_debug_state() + ))); + } + + self.jump_to(*entry_point_pc) + } + ExecutionMode::Suspendable => { + self.reset_execution_state(); + + if let Err(e) = self.validate_vm_state() { + return Err(VmError::Internal(alloc::format!( + "VM state validation failed before entry point execution: {} | {}", + e, + self.get_debug_state() + ))); + } + + self.execute_suspendable_entry(*entry_point_pc) + } + } + } + + pub fn execute_entry_point_by_name(&mut self, name: &str) -> Result { + let entry_point_pc = + self.program + .get_entry_point(name) + .ok_or_else(|| VmError::EntryPointNotFound { + name: String::from(name), + available: self.program.entry_points.keys().cloned().collect(), + })?; + + if entry_point_pc >= self.program.instructions.len() { + return Err(VmError::Internal(alloc::format!( + "Entry point PC {} >= instruction count {} for '{}' | {}", + entry_point_pc, + self.program.instructions.len(), + name, + self.get_debug_state() + ))); + } + + match self.execution_mode { + ExecutionMode::RunToCompletion => { + self.reset_execution_state(); + + if let Err(e) = self.validate_vm_state() { + return Err(VmError::Internal(alloc::format!( + "VM state validation failed before entry point execution: {} | {}", + e, + self.get_debug_state() + ))); + } + + self.jump_to(entry_point_pc) + } + ExecutionMode::Suspendable => { + self.reset_execution_state(); + + if let Err(e) = self.validate_vm_state() { + return Err(VmError::Internal(alloc::format!( + "VM state validation failed before entry point execution: {} | {}", + e, + self.get_debug_state() + ))); + } + + self.execute_suspendable_entry(entry_point_pc) + } + } + } + + pub(super) fn jump_to(&mut self, target: usize) -> Result { + let program = self.program.clone(); + self.pc = target; + while self.pc < program.instructions.len() { + if self.executed_instructions >= self.max_instructions { + return Err(VmError::InstructionLimitExceeded { + limit: self.max_instructions, + }); + } + + self.executed_instructions += 1; + let instruction = program.instructions[self.pc].clone(); + + match self.execute_instruction(&program, instruction)? { + InstructionOutcome::Continue => { + self.pc += 1; + } + InstructionOutcome::Return(value) => { + return Ok(value); + } + InstructionOutcome::Break => { + return Ok(self.registers[0].clone()); + } + InstructionOutcome::Suspend { reason } => { + return Err(VmError::Internal(alloc::format!( + "Suspend instruction {:?} is not supported in run-to-completion execution", + reason + ))); + } + } + } + + Ok(self.registers[0].clone()) + } + + fn execute_run_to_completion(&mut self) -> Result { + self.reset_execution_state(); + self.execution_state = ExecutionState::Running; + match self.jump_to(0) { + Ok(value) => { + self.execution_state = ExecutionState::Completed { + result: value.clone(), + }; + Ok(value) + } + Err(err) => { + self.execution_state = ExecutionState::Error { error: err.clone() }; + Err(err) + } + } + } + + fn execute_suspendable(&mut self) -> Result { + self.reset_execution_state(); + self.execution_state = ExecutionState::Running; + match self.run_stackless_from(0) { + Ok(result) => Ok(result), + Err(err) => { + self.execution_state = ExecutionState::Error { error: err.clone() }; + Err(err) + } + } + } + + fn execute_suspendable_entry(&mut self, entry_point_pc: usize) -> Result { + self.execution_state = ExecutionState::Running; + match self.run_stackless_from(entry_point_pc) { + Ok(result) => Ok(result), + Err(err) => { + self.execution_state = ExecutionState::Error { error: err.clone() }; + Err(err) + } + } + } + + pub fn resume(&mut self, resume_value: Option) -> Result { + let (reason, mut last_result) = match self.execution_state.clone() { + ExecutionState::Suspended { + reason, + last_result, + .. + } => (reason, last_result), + current_state => { + return Err(VmError::Internal(alloc::format!( + "Cannot resume VM when execution state is {:?}", + current_state + ))); + } + }; + + match reason.clone() { + SuspendReason::HostAwait { dest, .. } => { + let value = resume_value.ok_or_else(|| { + VmError::Internal("HostAwait suspension requires a resume value".into()) + })?; + + if self.registers.len() <= dest as usize { + self.registers.resize(dest as usize + 1, Value::Undefined); + } + self.registers[dest as usize] = value; + } + other_reason => { + if resume_value.is_some() { + return Err(VmError::Internal(alloc::format!( + "Unexpected resume value supplied for {:?}", + other_reason + ))); + } + } + } + + self.execution_state = ExecutionState::Running; + + let program = self.program.clone(); + self.run_stackless_loop(&program, &mut last_result)?; + + if matches!(self.execution_state, ExecutionState::Suspended { .. }) { + Ok(last_result) + } else { + self.execution_stack.clear(); + self.execution_state = ExecutionState::Completed { + result: last_result.clone(), + }; + Ok(last_result) + } + } + + fn run_stackless_from(&mut self, start_pc: usize) -> Result { + let program = self.program.clone(); + + self.execution_stack.clear(); + self.execution_stack.push(ExecutionFrame::main(start_pc, 0)); + + let mut last_result = self.registers.first().cloned().unwrap_or(Value::Undefined); + + self.run_stackless_loop(&program, &mut last_result)?; + + if matches!(self.execution_state, ExecutionState::Suspended { .. }) { + Ok(last_result) + } else { + self.execution_stack.clear(); + self.execution_state = ExecutionState::Completed { + result: last_result.clone(), + }; + Ok(last_result) + } + } + + fn run_stackless_loop(&mut self, program: &Program, last_result: &mut Value) -> Result<()> { + while !self.execution_stack.is_empty() { + self.frame_pc_overridden = false; + let should_finalize_rule = if let Some(frame) = self.execution_stack.last() { + matches!( + frame.kind, + FrameKind::Rule(RuleFrameData { + phase: RuleFramePhase::Finalizing, + .. + }) + ) + } else { + false + }; + + if should_finalize_rule { + let frame = self.execution_stack.pop().expect("frame available"); + self.finalize_rule_execution_frame(frame, last_result)?; + if self.execution_stack.is_empty() { + break; + } + continue; + } + + let frame_pc = { + let frame = self + .execution_stack + .last() + .expect("stack checked to be non-empty"); + frame.pc + }; + + if self.execution_mode == ExecutionMode::Suspendable + && self.breakpoints.contains(&frame_pc) + { + self.pc = frame_pc; + let snapshot = (*last_result).clone(); + self.execution_state = ExecutionState::Suspended { + reason: SuspendReason::Breakpoint { pc: frame_pc }, + pc: frame_pc, + last_result: snapshot, + }; + return Ok(()); + } + + if frame_pc >= program.instructions.len() { + let frame = self.execution_stack.pop().expect("frame exists"); + self.finalize_rule_execution_frame(frame, last_result)?; + if self.execution_stack.is_empty() { + break; + } + continue; + } + + if self.executed_instructions >= self.max_instructions { + self.execution_state = ExecutionState::Error { + error: VmError::InstructionLimitExceeded { + limit: self.max_instructions, + }, + }; + return Err(VmError::InstructionLimitExceeded { + limit: self.max_instructions, + }); + } + + self.pc = frame_pc; + let instruction = program.instructions[self.pc].clone(); + if let Some(frame_info) = self.execution_stack.last() { + if let FrameKind::Comprehension { context, .. } = &frame_info.kind { + if context.iteration_state.is_none() + && frame_pc == context.comprehension_end as usize + && !matches!(instruction, Instruction::ComprehensionEnd { .. }) + { + let resume_pc = frame_pc; + let _completed = self.execution_stack.pop().expect("frame exists"); + if let Some(parent) = self.execution_stack.last_mut() { + parent.pc = resume_pc; + self.frame_pc_overridden = true; + } + continue; + } + } + } + self.executed_instructions += 1; + + let stack_depth_before = self.execution_stack.len(); + + match self.execute_instruction(program, instruction) { + Ok(InstructionOutcome::Continue) => { + let stack_depth_after = self.execution_stack.len(); + if stack_depth_after == stack_depth_before && !self.frame_pc_overridden { + if let Some(frame) = self.execution_stack.last_mut() { + frame.pc = self.pc + 1; + } + } + if self.step_mode { + self.handle_instruction_suspend(SuspendReason::Step, &*last_result); + return Ok(()); + } + } + Ok(InstructionOutcome::Return(value)) => { + self.handle_instruction_return(value, last_result)?; + if self.execution_stack.is_empty() { + break; + } + } + Ok(InstructionOutcome::Break) => { + self.handle_instruction_break(last_result)?; + if self.execution_stack.is_empty() { + break; + } + } + Ok(InstructionOutcome::Suspend { reason }) => { + self.handle_instruction_suspend(reason, last_result); + return Ok(()); + } + Err(err) => { + if self.handle_instruction_error(err.clone(), last_result)? { + if self.execution_stack.is_empty() { + break; + } + continue; + } else { + self.execution_stack.clear(); + return Err(err); + } + } + } + } + + Ok(()) + } + + fn handle_instruction_suspend(&mut self, reason: SuspendReason, last_result: &Value) { + if let Some(frame) = self.execution_stack.last_mut() { + if !self.frame_pc_overridden && frame.pc <= self.pc { + frame.pc = self.pc + 1; + } + } + + self.execution_state = ExecutionState::Suspended { + reason, + pc: self.pc, + last_result: last_result.clone(), + }; + } + + fn handle_completed_frame_kind(&mut self, kind: FrameKind, last_result: &mut Value) { + match kind { + FrameKind::Main { + return_value_register, + } => { + *last_result = self + .registers + .get(return_value_register as usize) + .cloned() + .unwrap_or(Value::Undefined); + } + FrameKind::Loop { .. } | FrameKind::Comprehension { .. } => { + *last_result = self.registers.first().cloned().unwrap_or(Value::Undefined); + } + FrameKind::Rule(_) => { + *last_result = Value::Undefined; + } + } + } + + fn handle_instruction_return(&mut self, value: Value, last_result: &mut Value) -> Result<()> { + loop { + let frame = match self.execution_stack.pop() { + Some(frame) => frame, + None => { + *last_result = value; + return Ok(()); + } + }; + + match frame.kind { + FrameKind::Rule(mut data) => { + if self.registers.len() <= data.result_reg as usize { + self.registers + .resize(data.result_reg as usize + 1, Value::Undefined); + } + self.registers[data.result_reg as usize] = value.clone(); + data.accumulated_result = Some(value.clone()); + data.any_body_succeeded = true; + + let result = self.finalize_rule_frame_data(data)?; + *last_result = result.clone(); + + if let Some(parent_frame) = self.execution_stack.last_mut() { + parent_frame.pc = self.pc + 1; + } + return Ok(()); + } + FrameKind::Main { + return_value_register, + } => { + if self.registers.len() <= return_value_register as usize { + self.registers + .resize(return_value_register as usize + 1, Value::Undefined); + } + self.registers[return_value_register as usize] = value.clone(); + *last_result = value; + return Ok(()); + } + FrameKind::Loop { return_pc, .. } | FrameKind::Comprehension { return_pc, .. } => { + if let Some(parent_frame) = self.execution_stack.last_mut() { + parent_frame.pc = return_pc; + } + // Propagate the return value outward until we reach the owning frame + continue; + } + } + } + } + + fn handle_instruction_break(&mut self, last_result: &mut Value) -> Result<()> { + if let Some(frame) = self.execution_stack.pop() { + match frame.kind { + FrameKind::Rule(mut data) => { + let current_pc = frame.pc; + let next_pc = self.handle_rule_break_event(&mut data)?; + if let Some(pc) = next_pc { + self.execution_stack + .push(ExecutionFrame::new(pc, FrameKind::Rule(data))); + } else { + self.finalize_rule_execution_frame( + ExecutionFrame::new(current_pc, FrameKind::Rule(data)), + last_result, + )?; + } + } + other_kind => { + self.handle_break_non_rule( + ExecutionFrame::new(frame.pc, other_kind), + last_result, + ); + } + } + } + + Ok(()) + } + + fn handle_instruction_error(&mut self, _err: VmError, last_result: &mut Value) -> Result { + if let Some(frame) = self.execution_stack.pop() { + match frame.kind { + FrameKind::Rule(mut data) => { + let current_pc = frame.pc; + let next_pc = self.handle_rule_error_event(&mut data)?; + if let Some(pc) = next_pc { + self.execution_stack + .push(ExecutionFrame::new(pc, FrameKind::Rule(data))); + } else { + self.finalize_rule_execution_frame( + ExecutionFrame::new(current_pc, FrameKind::Rule(data)), + last_result, + )?; + } + return Ok(true); + } + other_kind => { + self.execution_stack + .push(ExecutionFrame::new(frame.pc, other_kind)); + } + } + } + + Ok(false) + } + + fn handle_break_non_rule(&mut self, frame: ExecutionFrame, last_result: &mut Value) { + match frame.kind { + FrameKind::Main { + return_value_register, + } => { + *last_result = self + .registers + .get(return_value_register as usize) + .cloned() + .unwrap_or(Value::Undefined); + } + FrameKind::Loop { return_pc, .. } | FrameKind::Comprehension { return_pc, .. } => { + if let Some(parent_frame) = self.execution_stack.last_mut() { + parent_frame.pc = return_pc; + } + *last_result = self.registers.first().cloned().unwrap_or(Value::Undefined); + } + FrameKind::Rule(_) => {} + } + } + + fn finalize_rule_execution_frame( + &mut self, + frame: ExecutionFrame, + last_result: &mut Value, + ) -> Result<()> { + match frame.kind { + FrameKind::Rule(data) => { + let result = self.finalize_rule_frame_data(data)?; + *last_result = result.clone(); + if let Some(parent_frame) = self.execution_stack.last_mut() { + parent_frame.pc = self.pc + 1; + } + } + other_kind => { + self.handle_completed_frame_kind(other_kind, last_result); + } + } + Ok(()) + } +} diff --git a/src/rvm/vm/execution_model.rs b/src/rvm/vm/execution_model.rs new file mode 100644 index 0000000..2400a76 --- /dev/null +++ b/src/rvm/vm/execution_model.rs @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::rvm::program::RuleType; +use crate::value::Value; +use alloc::collections::BTreeSet; +use alloc::vec::Vec; + +use super::context::{ComprehensionContext, LoopContext}; +use super::errors::VmError; + +/// Represents a single execution context (frame) in the VM +#[derive(Debug, Clone)] +pub(super) struct ExecutionFrame { + /// Program counter for this frame + pub(super) pc: usize, + /// Frame-specific payload + pub(super) kind: FrameKind, +} + +impl ExecutionFrame { + pub(super) fn new(pc: usize, kind: FrameKind) -> Self { + Self { pc, kind } + } + + pub(super) fn main(pc: usize, return_register: u8) -> Self { + Self { + pc, + kind: FrameKind::Main { + return_value_register: return_register, + }, + } + } +} + +/// Different categories of execution frames managed by the stackless engine +#[derive(Debug, Clone)] +pub(super) enum FrameKind { + /// Main entry frame used when executing the program start point + Main { return_value_register: u8 }, + /// Rule execution frame (replaces recursive jump_to calls) + Rule(RuleFrameData), + /// Loop iteration frame + Loop { + return_pc: usize, + context: LoopContext, + }, + /// Comprehension frame + Comprehension { + return_pc: usize, + context: ComprehensionContext, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum RuleFramePhase { + Initializing, + ExecutingDestructuring, + ExecutingBody, + Finalizing, +} + +#[derive(Debug, Clone)] +pub(super) struct RuleFrameData { + pub(super) return_pc: usize, + pub(super) dest_reg: u8, + pub(super) rule_index: u16, + pub(super) current_definition_index: usize, + pub(super) current_body_index: usize, + pub(super) total_definitions: usize, + pub(super) phase: RuleFramePhase, + pub(super) accumulated_result: Option, + pub(super) any_body_succeeded: bool, + pub(super) rule_failed_due_to_inconsistency: bool, + pub(super) rule_type: RuleType, + pub(super) result_reg: u8, + pub(super) is_function_rule: bool, + pub(super) num_registers: usize, + pub(super) num_retained_registers: usize, + pub(super) saved_registers: Vec, + pub(super) saved_loop_stack: Vec, + pub(super) saved_comprehension_stack: Vec, +} + +/// Explicit execution stack replacing the Rust call stack +#[derive(Debug, Clone, Default)] +pub(super) struct ExecutionStack { + frames: Vec, +} + +impl ExecutionStack { + pub fn new() -> Self { + Self { frames: Vec::new() } + } + + pub fn push(&mut self, frame: ExecutionFrame) { + self.frames.push(frame); + } + + pub fn pop(&mut self) -> Option { + self.frames.pop() + } + + pub fn last(&self) -> Option<&ExecutionFrame> { + self.frames.last() + } + + pub fn last_mut(&mut self) -> Option<&mut ExecutionFrame> { + self.frames.last_mut() + } + + pub fn is_empty(&self) -> bool { + self.frames.is_empty() + } + + pub fn len(&self) -> usize { + self.frames.len() + } + + pub fn get(&self, index: usize) -> Option<&ExecutionFrame> { + self.frames.get(index) + } + + pub fn get_mut(&mut self, index: usize) -> Option<&mut ExecutionFrame> { + self.frames.get_mut(index) + } + + pub fn clear(&mut self) { + self.frames.clear(); + } +} + +/// Represents the current execution state of the VM +#[derive(Debug, Clone, PartialEq, Default)] +pub enum ExecutionState { + #[default] + Ready, + Running, + Suspended { + reason: SuspendReason, + pc: usize, + last_result: Value, + }, + Completed { + result: Value, + }, + Error { + error: VmError, + }, +} + +/// Reasons why execution may suspend +#[derive(Debug, Clone, PartialEq)] +pub enum SuspendReason { + SuspendInstruction, + Breakpoint { + pc: usize, + }, + Step, + InstructionLimit, + External, + HostAwait { + dest: u8, + argument: Value, + identifier: Value, + }, +} + +/// Execution mode controls whether the VM runs straight through or supports suspension +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExecutionMode { + /// Execute in a single pass without exposing suspension points + RunToCompletion, + /// Execute cooperatively, allowing host-visible suspension and resume + Suspendable, +} + +/// Set of breakpoints used by the suspendable engine +pub(super) type BreakpointSet = BTreeSet; diff --git a/src/rvm/vm/functions.rs b/src/rvm/vm/functions.rs new file mode 100644 index 0000000..7898acf --- /dev/null +++ b/src/rvm/vm/functions.rs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +use crate::value::Value; +use alloc::string::String; +use alloc::vec::Vec; + +use super::errors::{Result, VmError}; +use super::execution_model::ExecutionMode; +use super::machine::RegoVM; + +impl RegoVM { + pub(super) fn execute_function_call(&mut self, params_index: u16) -> Result<()> { + let params = + self.program.instruction_data.function_call_params[params_index as usize].clone(); + match self.execution_mode { + ExecutionMode::RunToCompletion => { + self.execute_call_rule_common(params.dest, params.func_rule_index, Some(¶ms)) + } + ExecutionMode::Suspendable => self.execute_call_rule_suspendable( + params.dest, + params.func_rule_index, + Some(¶ms), + ), + } + } + + pub(super) fn execute_builtin_call(&mut self, params_index: u16) -> Result<()> { + let params = &self.program.instruction_data.builtin_call_params[params_index as usize]; + let builtin_info = &self.program.builtin_info_table[params.builtin_index as usize]; + + let mut args = Vec::new(); + for &arg_reg in params.arg_registers().iter() { + let arg_value = self.registers[arg_reg as usize].clone(); + args.push(arg_value); + } + + if (args.len() as u16) != builtin_info.num_args { + return Err(VmError::BuiltinArgumentMismatch { + expected: builtin_info.num_args, + actual: args.len(), + }); + } + + if let Some(builtin_fcn) = self.program.get_resolved_builtin(params.builtin_index) { + let dummy_source = crate::lexer::Source::from_contents("arg".into(), String::new())?; + let dummy_span = crate::lexer::Span { + source: dummy_source, + line: 1, + col: 1, + start: 0, + end: 3, + }; + + let mut dummy_exprs: Vec> = Vec::new(); + for _ in 0..args.len() { + let dummy_expr = crate::ast::Expr::Null { + span: dummy_span.clone(), + value: Value::Null, + eidx: 0, + }; + dummy_exprs.push(crate::ast::Ref::new(dummy_expr)); + } + + let result = (builtin_fcn.0)(&dummy_span, &dummy_exprs, &args, true)?; + self.registers[params.dest as usize] = result.clone(); + } else { + return Err(VmError::BuiltinNotResolved { + name: builtin_info.name.clone(), + }); + } + + Ok(()) + } +} diff --git a/src/rvm/vm/loops.rs b/src/rvm/vm/loops.rs new file mode 100644 index 0000000..928037f --- /dev/null +++ b/src/rvm/vm/loops.rs @@ -0,0 +1,661 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::rvm::instructions::LoopMode; +use crate::value::Value; + +use super::context::{IterationState, LoopContext}; +use super::errors::{Result, VmError}; +use super::execution_model::{ExecutionFrame, ExecutionMode, FrameKind}; +use super::machine::RegoVM; + +fn compute_body_resume_pc(loop_start_pc: usize, body_start: u16) -> usize { + if body_start == 0 { + return 0; + } + + let candidate = body_start.saturating_sub(1) as usize; + if candidate == loop_start_pc { + body_start as usize + } else { + candidate + } +} + +#[derive(Clone, Copy)] +pub(super) struct LoopParams { + pub(super) collection: u8, + pub(super) key_reg: u8, + pub(super) value_reg: u8, + pub(super) result_reg: u8, + pub(super) body_start: u16, + pub(super) loop_end: u16, +} + +#[derive(Debug)] +enum LoopAction { + ExitWithSuccess, + ExitWithFailure, + Continue, +} + +impl RegoVM { + pub(super) fn execute_loop_start(&mut self, mode: &LoopMode, params: LoopParams) -> Result<()> { + match self.execution_mode { + ExecutionMode::RunToCompletion => { + self.execute_loop_start_run_to_completion(mode, params) + } + ExecutionMode::Suspendable => self.execute_loop_start_suspendable(mode, params), + } + } + + pub(super) fn execute_loop_next(&mut self, body_start: u16, loop_end: u16) -> Result<()> { + match self.execution_mode { + ExecutionMode::RunToCompletion => { + self.execute_loop_next_run_to_completion(body_start, loop_end) + } + ExecutionMode::Suspendable => self.execute_loop_next_suspendable(body_start, loop_end), + } + } + + pub(super) fn handle_condition(&mut self, condition_passed: bool) -> Result<()> { + match self.execution_mode { + ExecutionMode::RunToCompletion => { + self.handle_condition_run_to_completion(condition_passed) + } + ExecutionMode::Suspendable => self.handle_condition_suspendable(condition_passed), + } + } + + fn execute_loop_start_run_to_completion( + &mut self, + mode: &LoopMode, + params: LoopParams, + ) -> Result<()> { + let initial_result = match mode { + LoopMode::Any | LoopMode::Every | LoopMode::ForEach => Value::Bool(false), + }; + self.registers[params.result_reg as usize] = initial_result.clone(); + + let collection_value = self.registers[params.collection as usize].clone(); + + let iteration_state = match &collection_value { + Value::Array(items) => { + if items.is_empty() { + self.handle_empty_collection(mode, params.result_reg, params.loop_end)?; + return Ok(()); + } + IterationState::Array { + items: items.clone(), + index: 0, + } + } + Value::Object(obj) => { + if obj.is_empty() { + self.handle_empty_collection(mode, params.result_reg, params.loop_end)?; + return Ok(()); + } + IterationState::Object { + obj: obj.clone(), + current_key: None, + first_iteration: true, + } + } + Value::Set(set) => { + if set.is_empty() { + self.handle_empty_collection(mode, params.result_reg, params.loop_end)?; + return Ok(()); + } + IterationState::Set { + items: set.clone(), + current_item: None, + first_iteration: true, + } + } + _ => { + self.handle_empty_collection(mode, params.result_reg, params.loop_end)?; + return Ok(()); + } + }; + + let has_next = + self.setup_next_iteration(&iteration_state, params.key_reg, params.value_reg)?; + if !has_next { + self.pc = params.loop_end as usize; + return Ok(()); + } + + let loop_next_pc = params.loop_end - 1; + let body_resume_pc = compute_body_resume_pc(self.pc, params.body_start); + + let loop_context = LoopContext { + mode: mode.clone(), + iteration_state, + key_reg: params.key_reg, + value_reg: params.value_reg, + result_reg: params.result_reg, + body_start: params.body_start, + loop_end: params.loop_end, + loop_next_pc, + body_resume_pc, + success_count: 0, + total_iterations: 0, + current_iteration_failed: false, + }; + + self.loop_stack.push(loop_context); + + self.pc = params.body_start as usize - 1; + + Ok(()) + } + + fn execute_loop_next_run_to_completion( + &mut self, + _body_start: u16, + loop_end: u16, + ) -> Result<()> { + if let Some(mut loop_ctx) = self.loop_stack.pop() { + let body_start = loop_ctx.body_start; + let loop_end = loop_ctx.loop_end; + + loop_ctx.total_iterations += 1; + + let iteration_succeeded = self.check_iteration_success(&loop_ctx)?; + + if iteration_succeeded { + loop_ctx.success_count += 1; + } + + let action = self.determine_loop_action(&loop_ctx.mode, iteration_succeeded); + + match action { + LoopAction::ExitWithSuccess => { + self.registers[loop_ctx.result_reg as usize] = Value::Bool(true); + self.pc = loop_end as usize - 1; + return Ok(()); + } + LoopAction::ExitWithFailure => { + self.registers[loop_ctx.result_reg as usize] = Value::Bool(false); + self.pc = loop_end as usize - 1; + return Ok(()); + } + LoopAction::Continue => {} + } + + if let IterationState::Object { + ref mut current_key, + .. + } = &mut loop_ctx.iteration_state + { + if loop_ctx.key_reg != loop_ctx.value_reg { + *current_key = Some(self.registers[loop_ctx.key_reg as usize].clone()); + } + } else if let IterationState::Set { + ref mut current_item, + .. + } = &mut loop_ctx.iteration_state + { + *current_item = Some(self.registers[loop_ctx.value_reg as usize].clone()); + } + + loop_ctx.iteration_state.advance(); + let has_next = self.setup_next_iteration( + &loop_ctx.iteration_state, + loop_ctx.key_reg, + loop_ctx.value_reg, + )?; + + if has_next { + loop_ctx.current_iteration_failed = false; + + self.loop_stack.push(loop_ctx); + self.pc = body_start as usize - 1; + } else { + let final_result = match loop_ctx.mode { + LoopMode::Any => Value::Bool(loop_ctx.success_count > 0), + LoopMode::Every => { + Value::Bool(loop_ctx.success_count == loop_ctx.total_iterations) + } + LoopMode::ForEach => Value::Bool(loop_ctx.success_count > 0), + }; + + self.registers[loop_ctx.result_reg as usize] = final_result; + + self.pc = loop_end as usize - 1; + } + + Ok(()) + } else { + self.pc = loop_end as usize; + Ok(()) + } + } + + fn execute_loop_start_suspendable( + &mut self, + mode: &LoopMode, + params: LoopParams, + ) -> Result<()> { + let initial_result = match mode { + LoopMode::Any | LoopMode::Every | LoopMode::ForEach => Value::Bool(false), + }; + self.registers[params.result_reg as usize] = initial_result.clone(); + + let collection_value = self.registers[params.collection as usize].clone(); + + let iteration_state = match &collection_value { + Value::Array(items) => { + if items.is_empty() { + self.handle_empty_collection(mode, params.result_reg, params.loop_end)?; + return Ok(()); + } + IterationState::Array { + items: items.clone(), + index: 0, + } + } + Value::Object(obj) => { + if obj.is_empty() { + self.handle_empty_collection(mode, params.result_reg, params.loop_end)?; + return Ok(()); + } + IterationState::Object { + obj: obj.clone(), + current_key: None, + first_iteration: true, + } + } + Value::Set(set) => { + if set.is_empty() { + self.handle_empty_collection(mode, params.result_reg, params.loop_end)?; + return Ok(()); + } + IterationState::Set { + items: set.clone(), + current_item: None, + first_iteration: true, + } + } + _ => { + self.handle_empty_collection(mode, params.result_reg, params.loop_end)?; + return Ok(()); + } + }; + + let has_next = + self.setup_next_iteration(&iteration_state, params.key_reg, params.value_reg)?; + if !has_next { + self.pc = params.loop_end as usize; + return Ok(()); + } + + let loop_next_pc = params.loop_end - 1; + let body_resume_pc = compute_body_resume_pc(self.pc, params.body_start); + + let loop_context = LoopContext { + mode: mode.clone(), + iteration_state, + key_reg: params.key_reg, + value_reg: params.value_reg, + result_reg: params.result_reg, + body_start: params.body_start, + loop_end: params.loop_end, + loop_next_pc, + body_resume_pc, + success_count: 0, + total_iterations: 0, + current_iteration_failed: false, + }; + + let frame = ExecutionFrame::new( + params.body_start as usize, + FrameKind::Loop { + return_pc: params.loop_end as usize, + context: loop_context, + }, + ); + self.execution_stack.push(frame); + + Ok(()) + } + + fn execute_loop_next_suspendable(&mut self, body_start: u16, loop_end: u16) -> Result<()> { + if !matches!( + self.execution_stack.last(), + Some(ExecutionFrame { + kind: FrameKind::Loop { .. }, + .. + }) + ) { + if let Some(frame) = self.execution_stack.last_mut() { + // Advance past the offending instruction so we do not repeatedly + // resume at the same LoopNext when the owning loop frame has + // already been popped (for example after a manual comprehension + // finalizes in suspendable mode). + let mut target_pc = loop_end as usize; + if target_pc <= self.pc { + target_pc = self.pc.saturating_add(1); + } + frame.pc = target_pc; + self.frame_pc_overridden = true; + } + return Ok(()); + } + + let (resume_pc, result_reg, loop_mode, iteration_succeeded) = { + let frame = self + .execution_stack + .last_mut() + .ok_or(VmError::AssertionFailed)?; + match &mut frame.kind { + FrameKind::Loop { return_pc, context } => { + context.total_iterations += 1; + let succeeded = !context.current_iteration_failed; + if succeeded { + context.success_count += 1; + } + + ( + *return_pc, + context.result_reg, + context.mode.clone(), + succeeded, + ) + } + _ => return Err(VmError::AssertionFailed), + } + }; + + let action = self.determine_loop_action(&loop_mode, iteration_succeeded); + + match action { + LoopAction::ExitWithSuccess => { + self.registers[result_reg as usize] = Value::Bool(true); + let completed_frame = self.execution_stack.pop().expect("loop frame exists"); + if let Some(parent) = self.execution_stack.last_mut() { + parent.pc = resume_pc; + self.frame_pc_overridden = true; + } + drop(completed_frame); + Ok(()) + } + LoopAction::ExitWithFailure => { + self.registers[result_reg as usize] = Value::Bool(false); + let completed_frame = self.execution_stack.pop().expect("loop frame exists"); + if let Some(parent) = self.execution_stack.last_mut() { + parent.pc = resume_pc; + self.frame_pc_overridden = true; + } + drop(completed_frame); + Ok(()) + } + LoopAction::Continue => { + let (mode, success_count, total_iterations, key_reg, value_reg, iteration_state) = { + let frame = self + .execution_stack + .last_mut() + .ok_or(VmError::AssertionFailed)?; + match &mut frame.kind { + FrameKind::Loop { context, .. } => { + if let IterationState::Object { + ref mut current_key, + .. + } = &mut context.iteration_state + { + if context.key_reg != context.value_reg { + *current_key = + Some(self.registers[context.key_reg as usize].clone()); + } + } else if let IterationState::Set { + ref mut current_item, + .. + } = &mut context.iteration_state + { + *current_item = + Some(self.registers[context.value_reg as usize].clone()); + } + + context.iteration_state.advance(); + context.current_iteration_failed = false; + + ( + context.mode.clone(), + context.success_count, + context.total_iterations, + context.key_reg, + context.value_reg, + context.iteration_state.clone(), + ) + } + _ => return Err(VmError::AssertionFailed), + } + }; + + let has_next = self.setup_next_iteration(&iteration_state, key_reg, value_reg)?; + + if has_next { + if let Some(frame) = self.execution_stack.last_mut() { + if let FrameKind::Loop { context, .. } = &frame.kind { + frame.pc = context.body_resume_pc; + } else { + frame.pc = body_start as usize; + } + self.frame_pc_overridden = true; + } + Ok(()) + } else { + let final_result = match mode { + LoopMode::Any => Value::Bool(success_count > 0), + LoopMode::Every => Value::Bool(success_count == total_iterations), + LoopMode::ForEach => Value::Bool(success_count > 0), + }; + + self.registers[result_reg as usize] = final_result; + + let completed_frame = self.execution_stack.pop().expect("loop frame exists"); + if let Some(parent) = self.execution_stack.last_mut() { + parent.pc = resume_pc; + self.frame_pc_overridden = true; + } + drop(completed_frame); + + Ok(()) + } + } + } + } + + fn handle_empty_collection( + &mut self, + mode: &LoopMode, + result_reg: u8, + loop_end: u16, + ) -> Result<()> { + let result = match mode { + LoopMode::Any => Value::Bool(false), + LoopMode::Every => Value::Bool(true), + LoopMode::ForEach => Value::Bool(false), + }; + + self.registers[result_reg as usize] = result; + self.pc = (loop_end as usize).saturating_sub(1); + Ok(()) + } + + pub(super) fn setup_next_iteration( + &mut self, + state: &IterationState, + key_reg: u8, + value_reg: u8, + ) -> Result { + match state { + IterationState::Array { items, index } => { + if *index < items.len() { + if key_reg != value_reg { + let key_value = Value::from(*index as f64); + self.registers[key_reg as usize] = key_value; + } + let item_value = items[*index].clone(); + self.registers[value_reg as usize] = item_value; + Ok(true) + } else { + Ok(false) + } + } + IterationState::Object { + obj, + current_key, + first_iteration, + } => { + if *first_iteration { + if let Some((key, value)) = obj.iter().next() { + if key_reg != value_reg { + self.registers[key_reg as usize] = key.clone(); + } + self.registers[value_reg as usize] = value.clone(); + Ok(true) + } else { + Ok(false) + } + } else if let Some(ref current) = current_key { + let mut range_iter = obj.range(( + core::ops::Bound::Excluded(current), + core::ops::Bound::Unbounded, + )); + if let Some((key, value)) = range_iter.next() { + if key_reg != value_reg { + self.registers[key_reg as usize] = key.clone(); + } + self.registers[value_reg as usize] = value.clone(); + Ok(true) + } else { + Ok(false) + } + } else { + Ok(false) + } + } + IterationState::Set { + items, + current_item, + first_iteration, + } => { + if *first_iteration { + if let Some(item) = items.iter().next() { + if key_reg != value_reg { + self.registers[key_reg as usize] = item.clone(); + } + self.registers[value_reg as usize] = item.clone(); + Ok(true) + } else { + Ok(false) + } + } else if let Some(ref current) = current_item { + let mut range_iter = items.range(( + core::ops::Bound::Excluded(current), + core::ops::Bound::Unbounded, + )); + if let Some(item) = range_iter.next() { + if key_reg != value_reg { + self.registers[key_reg as usize] = item.clone(); + } + self.registers[value_reg as usize] = item.clone(); + Ok(true) + } else { + Ok(false) + } + } else { + Ok(false) + } + } + } + } + + fn check_iteration_success(&self, loop_ctx: &LoopContext) -> Result { + Ok(!loop_ctx.current_iteration_failed) + } + + fn determine_loop_action(&self, mode: &LoopMode, success: bool) -> LoopAction { + match (mode, success) { + (LoopMode::Any, true) => LoopAction::ExitWithSuccess, + (LoopMode::Every, false) => LoopAction::ExitWithFailure, + (LoopMode::ForEach, _) => LoopAction::Continue, + _ => LoopAction::Continue, + } + } + + fn handle_condition_run_to_completion(&mut self, condition_passed: bool) -> Result<()> { + if condition_passed { + return Ok(()); + } + + if !self.loop_stack.is_empty() { + let (loop_mode, loop_next_pc, loop_end, result_reg) = { + let loop_ctx = self.loop_stack.last().unwrap(); + ( + loop_ctx.mode.clone(), + loop_ctx.loop_next_pc, + loop_ctx.loop_end, + loop_ctx.result_reg, + ) + }; + + match loop_mode { + LoopMode::Any => { + if let Some(loop_ctx_mut) = self.loop_stack.last_mut() { + loop_ctx_mut.current_iteration_failed = true; + } + + self.pc = loop_next_pc as usize - 1; + } + LoopMode::Every => { + self.loop_stack.pop(); + self.pc = loop_end as usize - 1; + self.registers[result_reg as usize] = Value::Bool(false); + } + _ => { + if let Some(loop_ctx_mut) = self.loop_stack.last_mut() { + loop_ctx_mut.current_iteration_failed = true; + } + self.pc = loop_next_pc as usize - 1; + } + } + } else { + return Err(VmError::AssertionFailed); + } + + Ok(()) + } + + fn handle_condition_suspendable(&mut self, condition_passed: bool) -> Result<()> { + if condition_passed { + return Ok(()); + } + + let (resume_pc, loop_ctx) = match self.execution_stack.last_mut() { + Some(ExecutionFrame { + kind: FrameKind::Loop { return_pc, context }, + .. + }) => (*return_pc, context), + _ => return Err(VmError::AssertionFailed), + }; + + match loop_ctx.mode { + LoopMode::Any | LoopMode::ForEach => { + loop_ctx.current_iteration_failed = true; + self.pc = loop_ctx.loop_next_pc as usize - 1; + } + LoopMode::Every => { + self.registers[loop_ctx.result_reg as usize] = Value::Bool(false); + let completed_frame = self.execution_stack.pop().expect("loop frame exists"); + if let Some(parent) = self.execution_stack.last_mut() { + parent.pc = resume_pc; + } + drop(completed_frame); + } + } + + Ok(()) + } +} diff --git a/src/rvm/vm/machine.rs b/src/rvm/vm/machine.rs new file mode 100644 index 0000000..e9d3478 --- /dev/null +++ b/src/rvm/vm/machine.rs @@ -0,0 +1,325 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::rvm::program::Program; +use crate::value::Value; +use crate::CompiledPolicy; +use alloc::collections::{btree_map::Entry, BTreeMap, VecDeque}; +use alloc::string::String; +use alloc::sync::Arc; +use alloc::vec; +use alloc::vec::Vec; + +use super::context::{CallRuleContext, ComprehensionContext, LoopContext}; +use super::errors::{Result, VmError}; +use super::execution_model::{ + BreakpointSet, ExecutionMode, ExecutionStack, ExecutionState, SuspendReason, +}; + +/// The Rego Virtual Machine +pub struct RegoVM { + /// Registers for storing values during execution + pub(super) registers: Vec, + + /// Program counter + pub(super) pc: usize, + + /// The compiled program containing instructions, literals, and metadata + pub(super) program: Arc, + + /// Reference to the compiled policy for default rule access + pub(super) compiled_policy: Option, + + /// Rule execution cache: rule_index -> (computed: bool, result: Value) + pub(super) rule_cache: Vec<(bool, Value)>, + + /// Global data object + pub(super) data: Value, + + /// Global input object + pub(super) input: Value, + + /// Loop execution stack + /// Note: Loops are either at the outermost level (rule body) or within the topmost comprehension. + /// Loops never contain comprehensions - it's always the other way around. + pub(super) loop_stack: Vec, + + /// Call rule execution stack for managing nested rule calls + pub(super) call_rule_stack: Vec, + + /// Register stack for isolated register spaces during rule calls + pub(super) register_stack: Vec>, + + /// Comprehension execution stack for tracking active comprehensions + /// Note: Comprehensions can be nested within each other, forming a proper nesting hierarchy. + /// Any loops within a comprehension belong to the topmost (current) comprehension context. + pub(super) comprehension_stack: Vec, + + /// Base register window size for the main execution context + pub(super) base_register_count: usize, + + /// Object pools for performance optimization + /// Pool of register windows for reuse during rule calls + pub(super) register_window_pool: Vec>, + + /// Maximum number of instructions to execute (default: 25000) + pub(super) max_instructions: usize, + + /// Current count of executed instructions + pub(super) executed_instructions: usize, + + /// Cache for evaluated paths in virtual data document lookup + /// Structure: evaluated[path_component1][path_component2]...[Undefined] = result_value + pub(super) evaluated: Value, + + /// Counter for cache hits during virtual data document lookup evaluation + pub(super) cache_hits: usize, + + /// Explicit execution stack used when running in suspendable mode + pub(super) execution_stack: ExecutionStack, + + /// Current execution state of the VM + pub(super) execution_state: ExecutionState, + + /// Active breakpoints for the suspendable engine + pub(super) breakpoints: BreakpointSet, + + /// Flag indicating whether single-step mode is active + pub(super) step_mode: bool, + + /// Preloaded responses for HostAwait in run-to-completion execution keyed by identifier + pub(super) host_await_responses: BTreeMap>, + + /// Current execution mode (run-to-completion vs suspendable) + pub(super) execution_mode: ExecutionMode, + + /// Tracks whether the current top-of-stack frame PC was explicitly set by an instruction + pub(super) frame_pc_overridden: bool, + + /// Whether builtins should raise errors strictly or return undefined on failure + pub(super) strict_builtin_errors: bool, +} + +impl Default for RegoVM { + fn default() -> Self { + Self::new() + } +} + +impl RegoVM { + /// Create a new virtual machine + pub fn new() -> Self { + RegoVM { + registers: Vec::new(), // Start with no registers - will be resized when program is loaded + pc: 0, + program: Arc::new(Program::default()), + compiled_policy: None, + rule_cache: Vec::new(), + data: Value::Null, + input: Value::Null, + loop_stack: Vec::new(), + call_rule_stack: Vec::new(), + register_stack: Vec::new(), + comprehension_stack: Vec::new(), + base_register_count: 2, // Default to 2 registers for basic operations + register_window_pool: Vec::new(), // Initialize register window pool + max_instructions: 25000, // Default maximum instruction limit + executed_instructions: 0, + evaluated: Value::new_object(), // Initialize evaluation cache + cache_hits: 0, // Initialize cache hit counter + execution_stack: ExecutionStack::new(), + execution_state: ExecutionState::Ready, + breakpoints: BreakpointSet::new(), + step_mode: false, + host_await_responses: BTreeMap::new(), + execution_mode: ExecutionMode::RunToCompletion, + frame_pc_overridden: false, + strict_builtin_errors: false, + } + } + + /// Create a new virtual machine with compiled policy for default rule support + pub fn new_with_policy(compiled_policy: CompiledPolicy) -> Self { + let mut vm = Self::new(); + vm.compiled_policy = Some(compiled_policy); + vm + } + + /// Load a complete program for execution + pub fn load_program(&mut self, program: Arc) { + self.program = program.clone(); + + // Use the dispatch window size from the program for initial register allocation + let dispatch_size = program.dispatch_window_size.max(2); // Ensure at least 2 registers + self.base_register_count = dispatch_size; + + // Resize registers to match program requirements + self.registers.clear(); + self.registers.resize(dispatch_size, Value::Undefined); + + // Initialize rule cache + self.rule_cache = vec![(false, Value::Undefined); program.rule_infos.len()]; + + // Set PC to main entry point + self.pc = program.main_entry_point; + self.executed_instructions = 0; // Reset instruction counter + } + + /// Set the compiled policy for default rule evaluation + pub fn set_compiled_policy(&mut self, compiled_policy: CompiledPolicy) { + self.compiled_policy = Some(compiled_policy); + } + + /// Set the maximum number of instructions that can be executed + pub fn set_max_instructions(&mut self, max: usize) { + self.max_instructions = max; + } + + /// Set the base register count for the main execution context + /// This determines how many registers are available in the root register window + pub fn set_base_register_count(&mut self, count: usize) { + self.base_register_count = count.max(1); // Ensure at least 1 register + if !self.registers.is_empty() { + self.registers + .resize(self.base_register_count, Value::Undefined); + } + } + + /// Set the global data object + pub fn set_data(&mut self, data: Value) -> Result<()> { + // Check for conflicts between rule tree and data + self.program.check_rule_data_conflicts(&data)?; + + self.data = data; + Ok(()) + } + + /// Set the global input object + pub fn set_input(&mut self, input: Value) { + self.input = input; + } + + /// Get the number of entry points available + pub fn get_entry_point_count(&self) -> usize { + self.program.entry_points.len() + } + + /// Get all entry point names + pub fn get_entry_point_names(&self) -> Vec { + self.program.entry_points.keys().cloned().collect() + } + + // Public getters for visualization + pub fn get_pc(&self) -> usize { + self.pc + } + + pub fn get_registers(&self) -> &Vec { + &self.registers + } + + pub fn get_program(&self) -> &Arc { + &self.program + } + + pub fn get_call_stack(&self) -> &Vec { + &self.call_rule_stack + } + + pub fn get_loop_stack(&self) -> &Vec { + &self.loop_stack + } + + pub fn get_cache_hits(&self) -> usize { + self.cache_hits + } + + /// Set the execution mode for the VM + pub fn set_execution_mode(&mut self, mode: ExecutionMode) { + self.execution_mode = mode; + } + + /// Configure whether builtin operations should raise errors strictly + pub fn set_strict_builtin_errors(&mut self, strict: bool) { + self.strict_builtin_errors = strict; + } + + /// Returns whether builtin operations raise errors strictly + pub fn strict_builtin_errors(&self) -> bool { + self.strict_builtin_errors + } + + /// Enable or disable single-step execution for suspendable runs + pub fn set_step_mode(&mut self, enabled: bool) { + self.step_mode = enabled; + } + + /// Configure the sequence of HostAwait responses for run-to-completion execution + pub fn set_host_await_responses(&mut self, responses: I) + where + I: IntoIterator, + J: IntoIterator, + { + self.host_await_responses.clear(); + + for (identifier, values) in responses { + let mut queue = VecDeque::new(); + queue.extend(values); + + match self.host_await_responses.entry(identifier) { + Entry::Vacant(entry) => { + entry.insert(queue); + } + Entry::Occupied(mut entry) => { + entry.get_mut().extend(queue); + } + } + } + } + + pub(super) fn next_host_await_response( + &mut self, + identifier: &Value, + dest: u8, + ) -> Result { + let missing_error = || VmError::HostAwaitResponseMissing { + dest, + identifier: identifier.clone(), + }; + + let (response, should_remove) = { + let queue = self + .host_await_responses + .get_mut(identifier) + .ok_or_else(missing_error)?; + + let response = queue.pop_front().ok_or_else(missing_error)?; + let should_remove = queue.is_empty(); + (response, should_remove) + }; + + if should_remove { + self.host_await_responses.remove(identifier); + } + + Ok(response) + } + + /// Get the current execution mode + pub fn get_execution_mode(&self) -> ExecutionMode { + self.execution_mode + } + + /// Get the current execution state of the VM + pub fn execution_state(&self) -> &ExecutionState { + &self.execution_state + } + + /// Get the suspend reason if the VM is currently suspended + pub fn suspend_reason(&self) -> Option<&SuspendReason> { + match &self.execution_state { + ExecutionState::Suspended { reason, .. } => Some(reason), + _ => None, + } + } +} diff --git a/src/rvm/vm/mod.rs b/src/rvm/vm/mod.rs new file mode 100644 index 0000000..695f920 --- /dev/null +++ b/src/rvm/vm/mod.rs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +extern crate alloc; + +mod arithmetic; +mod comprehension; +mod context; +mod dispatch; +mod errors; +mod execution; +mod execution_model; +mod functions; +mod loops; +mod machine; +mod rules; +mod state; +mod virtual_data; + +pub use context::{CallRuleContext, IterationState, LoopContext}; +pub use errors::{Result, VmError}; +pub use execution_model::{ExecutionMode, ExecutionState, SuspendReason}; +pub use machine::RegoVM; diff --git a/src/rvm/vm/rules.rs b/src/rvm/vm/rules.rs new file mode 100644 index 0000000..31bef34 --- /dev/null +++ b/src/rvm/vm/rules.rs @@ -0,0 +1,627 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::rvm::instructions::FunctionCallParams; +use crate::rvm::program::{RuleInfo, RuleType}; +use crate::value::Value; +use alloc::vec::Vec; +use core::mem; + +use super::context::CallRuleContext; +use super::errors::{Result, VmError}; +use super::execution_model::{ + ExecutionFrame, ExecutionMode, FrameKind, RuleFrameData, RuleFramePhase, +}; +use super::machine::RegoVM; + +impl RegoVM { + pub(super) fn execute_rule_definitions_common( + &mut self, + rule_definitions: &[Vec], + rule_info: &RuleInfo, + function_call_params: Option<&FunctionCallParams>, + ) -> Result<(Value, bool)> { + let mut first_successful_result: Option = None; + let mut rule_failed_due_to_inconsistency = false; + let is_function_call = rule_info.function_info.is_some(); + let result_reg = rule_info.result_reg as usize; + + let num_registers = rule_info.num_registers as usize; + let mut register_window = self.new_register_window(); + register_window.clear(); + register_window.reserve(num_registers); + + register_window.push(Value::Undefined); + + let num_retained_registers = match function_call_params { + Some(params) => { + for arg in params.args[0..params.num_args as usize].iter() { + register_window.push(self.registers[*arg as usize].clone()); + } + params.num_args as usize + 1 + } + _ => match rule_info.rule_type { + RuleType::PartialSet | RuleType::PartialObject => 1, + RuleType::Complete => 0, + }, + }; + + let mut old_registers = Vec::default(); + mem::swap(&mut old_registers, &mut self.registers); + + let mut old_loop_stack = Vec::default(); + mem::swap(&mut old_loop_stack, &mut self.loop_stack); + + let mut old_comprehension_stack = Vec::default(); + mem::swap(&mut old_comprehension_stack, &mut self.comprehension_stack); + + self.register_stack.push(old_registers); + self.registers = register_window; + + 'outer: for (def_idx, definition_bodies) in rule_definitions.iter().enumerate() { + for (body_entry_point_idx, body_entry_point) in definition_bodies.iter().enumerate() { + if let Some(ctx) = self.call_rule_stack.last_mut() { + ctx.current_body_index = body_entry_point_idx; + ctx.current_definition_index = def_idx; + } + + self.registers + .resize(num_retained_registers, Value::Undefined); + self.registers.resize(num_registers, Value::Undefined); + + if let Some(destructuring_entry_point) = + rule_info.destructuring_blocks.get(def_idx).and_then(|x| *x) + { + match self.jump_to(destructuring_entry_point as usize) { + Ok(_result) => {} + Err(_e) => { + continue 'outer; + } + } + } + + match self.jump_to(*body_entry_point as usize) { + Ok(_) => { + if matches!(rule_info.rule_type, RuleType::Complete) || is_function_call { + let current_result = self.registers[result_reg].clone(); + if current_result != Value::Undefined { + if let Some(ref expected) = first_successful_result { + if *expected != current_result { + rule_failed_due_to_inconsistency = true; + self.registers[result_reg] = Value::Undefined; + break; + } + } else { + first_successful_result = Some(current_result.clone()); + } + } + } + } + Err(_e) => { + continue; + } + } + } + + if rule_failed_due_to_inconsistency { + break; + } + } + + let final_result = if rule_failed_due_to_inconsistency { + Value::Undefined + } else if let Some(successful_result) = first_successful_result { + successful_result + } else { + self.registers[result_reg].clone() + }; + + if let Some(old_registers) = self.register_stack.pop() { + let mut current_register_window = Vec::default(); + mem::swap(&mut current_register_window, &mut self.registers); + self.return_register_window(current_register_window); + + self.registers = old_registers; + } + + self.loop_stack = old_loop_stack; + self.comprehension_stack = old_comprehension_stack; + + Ok((final_result, rule_failed_due_to_inconsistency)) + } + + pub(super) fn execute_call_rule_common( + &mut self, + dest: u8, + rule_index: u16, + function_call_params: Option<&FunctionCallParams>, + ) -> Result<()> { + let rule_idx = rule_index as usize; + + if rule_idx >= self.rule_cache.len() { + return Err(VmError::RuleIndexOutOfBounds { index: rule_index }); + } + + let rule_info = self + .program + .rule_infos + .get(rule_idx) + .ok_or(VmError::RuleInfoMissing { index: rule_index })? + .clone(); + + let is_function_rule = rule_info.function_info.is_some(); + + if !is_function_rule { + let (computed, cached_result) = &self.rule_cache[rule_idx]; + if *computed { + self.registers[dest as usize] = cached_result.clone(); + return Ok(()); + } + } + + let rule_type = rule_info.rule_type.clone(); + let rule_definitions = rule_info.definitions.clone(); + + if rule_definitions.is_empty() { + let result = Value::Undefined; + if !is_function_rule { + self.rule_cache[rule_idx] = (true, result.clone()); + } + self.registers[dest as usize] = result; + return Ok(()); + } + + self.call_rule_stack.push(CallRuleContext { + return_pc: self.pc, + dest_reg: dest, + result_reg: rule_info.result_reg, + rule_index, + rule_type: rule_type.clone(), + current_definition_index: 0, + current_body_index: 0, + }); + + let (final_result, rule_failed_due_to_inconsistency) = self + .execute_rule_definitions_common(&rule_definitions, &rule_info, function_call_params)?; + + self.registers[dest as usize] = Value::Undefined; + + let call_context = self.call_rule_stack.pop().expect("Call stack underflow"); + self.pc = call_context.return_pc; + + let result_from_rule = if !rule_failed_due_to_inconsistency { + final_result + } else { + Value::Undefined + }; + + self.registers[dest as usize] = result_from_rule.clone(); + + if self.registers[dest as usize] == Value::Undefined && !rule_failed_due_to_inconsistency { + match call_context.rule_type { + RuleType::PartialSet => { + self.registers[dest as usize] = Value::new_set(); + } + RuleType::PartialObject => { + self.registers[dest as usize] = Value::new_object(); + } + RuleType::Complete => { + if let Some(rule_info) = self + .program + .rule_infos + .get(call_context.rule_index as usize) + { + if let Some(default_literal_index) = rule_info.default_literal_index { + if let Some(default_value) = + self.program.literals.get(default_literal_index as usize) + { + self.registers[dest as usize] = default_value.clone(); + } + } + } + } + } + } + + let final_result = self.registers[dest as usize].clone(); + if !is_function_rule { + self.rule_cache[rule_idx] = (true, final_result); + } + Ok(()) + } + + pub(super) fn execute_call_rule(&mut self, dest: u8, rule_index: u16) -> Result<()> { + match self.execution_mode { + ExecutionMode::RunToCompletion => self.execute_call_rule_common(dest, rule_index, None), + ExecutionMode::Suspendable => { + self.execute_call_rule_suspendable(dest, rule_index, None) + } + } + } + + pub(super) fn execute_call_rule_suspendable( + &mut self, + dest: u8, + rule_index: u16, + function_call_params: Option<&FunctionCallParams>, + ) -> Result<()> { + let rule_idx = rule_index as usize; + + if rule_idx >= self.rule_cache.len() { + return Err(VmError::RuleIndexOutOfBounds { index: rule_index }); + } + + let rule_info = self + .program + .rule_infos + .get(rule_idx) + .ok_or(VmError::RuleInfoMissing { index: rule_index })? + .clone(); + + let is_function_rule = rule_info.function_info.is_some(); + + if !is_function_rule { + let (computed, cached_result) = &self.rule_cache[rule_idx]; + if *computed { + self.registers[dest as usize] = cached_result.clone(); + return Ok(()); + } + } + + if rule_info.definitions.is_empty() { + let result = Value::Undefined; + if !is_function_rule { + self.rule_cache[rule_idx] = (true, result.clone()); + } + if self.registers.len() <= dest as usize { + self.registers.resize(dest as usize + 1, Value::Undefined); + } + self.registers[dest as usize] = result; + return Ok(()); + } + + let num_registers = rule_info.num_registers as usize; + + let num_retained_registers = match function_call_params { + Some(params) => params.arg_count() + 1, + None => match rule_info.rule_type { + RuleType::PartialSet | RuleType::PartialObject => 1, + RuleType::Complete => 0, + }, + }; + + let mut register_window = self.new_register_window(); + register_window.clear(); + register_window.reserve(num_registers); + register_window.push(Value::Undefined); + + if let Some(params) = function_call_params { + for &arg in params.arg_registers() { + register_window.push(self.registers[arg as usize].clone()); + } + } + + let mut saved_registers = Vec::default(); + mem::swap(&mut saved_registers, &mut self.registers); + self.registers = register_window; + + let mut saved_loop_stack = Vec::default(); + mem::swap(&mut saved_loop_stack, &mut self.loop_stack); + + let mut saved_comprehension_stack = Vec::default(); + mem::swap( + &mut saved_comprehension_stack, + &mut self.comprehension_stack, + ); + + self.loop_stack.clear(); + self.comprehension_stack.clear(); + + self.call_rule_stack.push(CallRuleContext { + return_pc: self.pc, + dest_reg: dest, + result_reg: rule_info.result_reg, + rule_index, + rule_type: rule_info.rule_type.clone(), + current_definition_index: 0, + current_body_index: 0, + }); + + let mut frame_data = RuleFrameData { + return_pc: self.pc, + dest_reg: dest, + rule_index, + current_definition_index: 0, + current_body_index: 0, + total_definitions: rule_info.definitions.len(), + phase: RuleFramePhase::Initializing, + accumulated_result: None, + any_body_succeeded: false, + rule_failed_due_to_inconsistency: false, + rule_type: rule_info.rule_type.clone(), + result_reg: rule_info.result_reg, + is_function_rule, + num_registers, + num_retained_registers, + saved_registers, + saved_loop_stack, + saved_comprehension_stack, + }; + + let initial_pc = self + .prepare_rule_frame_initial_pc(&mut frame_data, &rule_info)? + .ok_or_else(|| VmError::Internal("Rule frame has no initial PC".into()))?; + + let frame = ExecutionFrame::new(initial_pc, FrameKind::Rule(frame_data)); + self.execution_stack.push(frame); + + Ok(()) + } + + pub(super) fn execute_rule_init(&mut self, result_reg: u8, _rule_index: u16) -> Result<()> { + let current_ctx = self + .call_rule_stack + .last_mut() + .expect("Call stack underflow"); + current_ctx.result_reg = result_reg; + match current_ctx.rule_type { + RuleType::Complete => { + self.registers[result_reg as usize] = Value::Undefined; + } + RuleType::PartialSet => { + if current_ctx.current_definition_index == 0 && current_ctx.current_body_index == 0 + { + self.registers[result_reg as usize] = Value::new_set(); + } + } + RuleType::PartialObject => { + if current_ctx.current_definition_index == 0 && current_ctx.current_body_index == 0 + { + self.registers[result_reg as usize] = Value::new_object(); + } + } + } + Ok(()) + } + + pub(super) fn execute_rule_return(&mut self) -> Result<()> { + Ok(()) + } + + fn prepare_rule_frame_initial_pc( + &mut self, + frame_data: &mut RuleFrameData, + rule_info: &RuleInfo, + ) -> Result> { + frame_data.current_definition_index = 0; + frame_data.current_body_index = 0; + frame_data.phase = RuleFramePhase::Initializing; + self.rule_frame_schedule_segment(frame_data, rule_info) + } + + fn rule_frame_schedule_segment( + &mut self, + frame_data: &mut RuleFrameData, + rule_info: &RuleInfo, + ) -> Result> { + if frame_data.rule_failed_due_to_inconsistency { + frame_data.phase = RuleFramePhase::Finalizing; + return Ok(None); + } + + while frame_data.current_definition_index < frame_data.total_definitions { + let definition_bodies = &rule_info.definitions[frame_data.current_definition_index]; + + if frame_data.current_body_index < definition_bodies.len() { + if let Some(ctx) = self.call_rule_stack.last_mut() { + ctx.current_definition_index = frame_data.current_definition_index; + ctx.current_body_index = frame_data.current_body_index; + } + + self.registers + .resize(frame_data.num_retained_registers, Value::Undefined); + self.registers + .resize(frame_data.num_registers, Value::Undefined); + + if let Some(destructuring_entry_point) = rule_info + .destructuring_blocks + .get(frame_data.current_definition_index) + .and_then(|opt| *opt) + { + frame_data.phase = RuleFramePhase::ExecutingDestructuring; + return Ok(Some(destructuring_entry_point as usize)); + } else { + frame_data.phase = RuleFramePhase::ExecutingBody; + return Ok(Some( + definition_bodies[frame_data.current_body_index] as usize, + )); + } + } else { + frame_data.current_definition_index += 1; + frame_data.current_body_index = 0; + } + } + + frame_data.phase = RuleFramePhase::Finalizing; + Ok(None) + } + + fn rule_frame_after_destructuring_success( + &mut self, + frame_data: &mut RuleFrameData, + rule_info: &RuleInfo, + ) -> Result> { + frame_data.phase = RuleFramePhase::ExecutingBody; + let definition_bodies = &rule_info.definitions[frame_data.current_definition_index]; + if frame_data.current_body_index >= definition_bodies.len() { + frame_data.current_body_index += 1; + return self.rule_frame_schedule_segment(frame_data, rule_info); + } + + Ok(Some( + definition_bodies[frame_data.current_body_index] as usize, + )) + } + + fn rule_frame_after_failure( + &mut self, + frame_data: &mut RuleFrameData, + rule_info: &RuleInfo, + ) -> Result> { + frame_data.current_body_index += 1; + self.rule_frame_schedule_segment(frame_data, rule_info) + } + + fn rule_frame_after_success( + &mut self, + frame_data: &mut RuleFrameData, + rule_info: &RuleInfo, + ) -> Result> { + frame_data.any_body_succeeded = true; + + if matches!(frame_data.rule_type, RuleType::Complete) || frame_data.is_function_rule { + let current_result = self + .registers + .get(frame_data.result_reg as usize) + .cloned() + .unwrap_or(Value::Undefined); + + if current_result != Value::Undefined { + if let Some(expected) = &frame_data.accumulated_result { + if *expected != current_result { + frame_data.rule_failed_due_to_inconsistency = true; + if let Some(result_slot) = + self.registers.get_mut(frame_data.result_reg as usize) + { + *result_slot = Value::Undefined; + } + } + } else { + frame_data.accumulated_result = Some(current_result); + } + } + } + + frame_data.current_body_index += 1; + self.rule_frame_schedule_segment(frame_data, rule_info) + } + + pub(super) fn finalize_rule_frame_data(&mut self, frame_data: RuleFrameData) -> Result { + let RuleFrameData { + return_pc, + dest_reg, + rule_index, + accumulated_result, + rule_failed_due_to_inconsistency, + rule_type, + result_reg, + is_function_rule, + saved_registers, + saved_loop_stack, + saved_comprehension_stack, + .. + } = frame_data; + + let rule_idx = rule_index as usize; + let rule_info = self + .program + .rule_infos + .get(rule_idx) + .ok_or(VmError::RuleInfoMissing { index: rule_index })? + .clone(); + + let result_from_rule = if rule_failed_due_to_inconsistency { + Value::Undefined + } else if let Some(value) = accumulated_result { + value + } else { + self.registers + .get(result_reg as usize) + .cloned() + .unwrap_or(Value::Undefined) + }; + + let mut current_window = Vec::default(); + mem::swap(&mut current_window, &mut self.registers); + self.return_register_window(current_window); + + self.loop_stack = saved_loop_stack; + self.comprehension_stack = saved_comprehension_stack; + + let mut parent_registers = saved_registers; + if parent_registers.len() <= dest_reg as usize { + parent_registers.resize(dest_reg as usize + 1, Value::Undefined); + } + parent_registers[dest_reg as usize] = result_from_rule.clone(); + + if parent_registers[dest_reg as usize] == Value::Undefined + && !rule_failed_due_to_inconsistency + { + match rule_type { + RuleType::PartialSet => parent_registers[dest_reg as usize] = Value::new_set(), + RuleType::PartialObject => { + parent_registers[dest_reg as usize] = Value::new_object() + } + RuleType::Complete => { + if let Some(default_literal_index) = rule_info.default_literal_index { + if let Some(default_value) = + self.program.literals.get(default_literal_index as usize) + { + parent_registers[dest_reg as usize] = default_value.clone(); + } + } + } + } + } + + let final_value = parent_registers[dest_reg as usize].clone(); + + if !is_function_rule { + self.rule_cache[rule_idx] = (true, final_value.clone()); + } + + self.registers = parent_registers; + + if self.call_rule_stack.pop().is_none() { + return Err(VmError::Internal(alloc::format!( + "Call rule stack underflow during rule finalization | {}", + self.get_debug_state() + ))); + } + + self.pc = return_pc; + + Ok(final_value) + } + + pub(super) fn handle_rule_break_event( + &mut self, + frame_data: &mut RuleFrameData, + ) -> Result> { + let rule_info = self.get_rule_info(frame_data.rule_index)?; + match frame_data.phase { + RuleFramePhase::ExecutingDestructuring => { + self.rule_frame_after_destructuring_success(frame_data, &rule_info) + } + RuleFramePhase::ExecutingBody => self.rule_frame_after_success(frame_data, &rule_info), + RuleFramePhase::Initializing | RuleFramePhase::Finalizing => Ok(None), + } + } + + pub(super) fn handle_rule_error_event( + &mut self, + frame_data: &mut RuleFrameData, + ) -> Result> { + let rule_info = self.get_rule_info(frame_data.rule_index)?; + self.rule_frame_after_failure(frame_data, &rule_info) + } + + fn get_rule_info(&self, rule_index: u16) -> Result { + let idx = rule_index as usize; + self.program + .rule_infos + .get(idx) + .cloned() + .ok_or(VmError::RuleInfoMissing { index: rule_index }) + } +} diff --git a/src/rvm/vm/state.rs b/src/rvm/vm/state.rs new file mode 100644 index 0000000..86e307a --- /dev/null +++ b/src/rvm/vm/state.rs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::value::Value; +use alloc::string::String; +use alloc::vec::Vec; + +use super::errors::{Result, VmError}; +use super::execution_model::ExecutionState; +use super::machine::RegoVM; + +impl RegoVM { + /// Reset all execution state and return objects to pools for reuse + pub(super) fn reset_execution_state(&mut self) { + // Reset basic execution state + self.executed_instructions = 0; + self.pc = 0; + self.evaluated = Value::new_object(); + self.cache_hits = 0; + + // Reset suspendable execution state + self.execution_stack.clear(); + self.execution_state = ExecutionState::Ready; + + // Return objects to pools and clear stacks + self.return_to_pools(); + + // Reset rule cache + self.rule_cache = alloc::vec![(false, Value::Undefined); self.program.rule_infos.len()]; + + // Reset registers to clean state + self.registers.clear(); + self.registers + .resize(self.base_register_count, Value::Undefined); + } + + /// Return all active objects to their respective pools for reuse + pub(super) fn return_to_pools(&mut self) { + // Clear stacks - these are small structs that don't need pooling + self.loop_stack.clear(); + self.call_rule_stack.clear(); + self.comprehension_stack.clear(); + + // Return register windows to pool for reuse + while let Some(registers) = self.register_stack.pop() { + self.return_register_window(registers); + } + } + + /// Get a register window from the pool or create a new one + pub(super) fn new_register_window(&mut self) -> Vec { + self.register_window_pool.pop().unwrap_or_default() + } + + /// Return a register window to the pool for reuse + pub(super) fn return_register_window(&mut self, mut window: Vec) { + window.clear(); // Clear contents for reuse + self.register_window_pool.push(window); + } + + /// Validate VM state consistency for debugging + pub(super) fn validate_vm_state(&self) -> Result<()> { + // Check register bounds + if self.registers.len() < self.base_register_count { + return Err(VmError::Internal(alloc::format!( + "Register count {} < base count {}", + self.registers.len(), + self.base_register_count + ))); + } + + // Check PC bounds + if self.pc >= self.program.instructions.len() { + return Err(VmError::Internal(alloc::format!( + "PC {} >= instruction count {}", + self.pc, + self.program.instructions.len() + ))); + } + + // Check rule cache bounds + if self.rule_cache.len() != self.program.rule_infos.len() { + return Err(VmError::Internal(alloc::format!( + "Rule cache size {} != rule info count {}", + self.rule_cache.len(), + self.program.rule_infos.len() + ))); + } + + Ok(()) + } + + /// Get current VM state for debugging + pub(super) fn get_debug_state(&self) -> String { + alloc::format!( + "VM State: PC={}, registers={}, executed={}/{}, stacks: loop={}, call={}, register={}, comprehension={}", + self.pc, + self.registers.len(), + self.executed_instructions, + self.max_instructions, + self.loop_stack.len(), + self.call_rule_stack.len(), + self.register_stack.len(), + self.comprehension_stack.len() + ) + } +} diff --git a/src/rvm/vm/virtual_data.rs b/src/rvm/vm/virtual_data.rs new file mode 100644 index 0000000..9803ad8 --- /dev/null +++ b/src/rvm/vm/virtual_data.rs @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::rvm::instructions::LiteralOrRegister; +use crate::value::Value; +use alloc::vec::Vec; + +use super::errors::{Result, VmError}; +use super::machine::RegoVM; + +impl RegoVM { + pub(super) fn execute_virtual_data_document_lookup_subobject( + &mut self, + path_components: &[LiteralOrRegister], + rule_tree_subobject: &Value, + ) -> Result { + let mut root_path = Vec::new(); + for component in path_components { + let key_value = match component { + LiteralOrRegister::Literal(idx) => self + .program + .literals + .get(*idx as usize) + .ok_or(VmError::LiteralIndexOutOfBounds { + index: *idx as usize, + })? + .clone(), + LiteralOrRegister::Register(reg) => self.registers[*reg as usize].clone(), + }; + root_path.push(key_value); + } + + let mut data_subobject = self.data.clone(); + for path_component in &root_path { + data_subobject = data_subobject[path_component].clone(); + } + + let mut result_subobject = match data_subobject { + Value::Undefined => Value::new_object(), + _ => data_subobject, + }; + + self.traverse_rule_tree_subobject(rule_tree_subobject, &mut result_subobject, &root_path)?; + + Ok(result_subobject) + } + + fn set_nested_value(&self, target: &mut Value, path: &[Value], value: Value) -> Result<()> { + Self::set_nested_value_static(target, path, value) + } + + fn set_nested_value_static(target: &mut Value, path: &[Value], value: Value) -> Result<()> { + if path.is_empty() { + *target = value; + return Ok(()); + } + + if *target == Value::Undefined { + *target = Value::new_object(); + } + + if let Value::Object(ref mut map) = target { + let key = &path[0]; + + if !map.contains_key(key) { + crate::Rc::make_mut(map).insert(key.clone(), Value::Undefined); + } + + if let Some(next_target) = crate::Rc::make_mut(map).get_mut(key) { + Self::set_nested_value_static(next_target, &path[1..], value)?; + } + } else { + return Err(VmError::InvalidRuleTreeEntry { + value: target.clone(), + }); + } + + Ok(()) + } + + fn traverse_rule_tree_subobject( + &mut self, + rule_tree_node: &Value, + result_subobject: &mut Value, + root_path: &[Value], + ) -> Result<()> { + self.traverse_rule_tree_subobject_with_path( + rule_tree_node, + result_subobject, + root_path, + &[], + ) + } + + fn traverse_rule_tree_subobject_with_path( + &mut self, + rule_tree_node: &Value, + result_subobject: &mut Value, + root_path: &[Value], + relative_path: &[Value], + ) -> Result<()> { + match rule_tree_node { + Value::Number(rule_idx) => { + if let Some(rule_index) = rule_idx.as_u64() { + let mut full_cache_path = root_path.to_vec(); + full_cache_path.extend_from_slice(relative_path); + + let cached_result = { + let mut cache_lookup = &self.evaluated; + let mut path_exists = true; + + for path_component in &full_cache_path { + if let Value::Object(ref map) = cache_lookup { + if let Some(next_value) = map.get(path_component) { + cache_lookup = next_value; + } else { + path_exists = false; + break; + } + } else { + path_exists = false; + break; + } + } + + if path_exists { + if let Value::Object(ref map) = cache_lookup { + map.get(&Value::Undefined).cloned() + } else { + None + } + } else { + None + } + }; + + let rule_result = if let Some(cached) = cached_result { + self.cache_hits += 1; + cached + } else { + let temp_reg = self.registers.len() as u8; + self.registers.push(Value::Undefined); + self.execute_call_rule_common(temp_reg, rule_index as u16, None)?; + let result = self.registers.pop().unwrap(); + + let mut cache_path = full_cache_path.clone(); + cache_path.push(Value::Undefined); + Self::set_nested_value_static( + &mut self.evaluated, + &cache_path, + result.clone(), + )?; + + result + }; + + self.set_nested_value(result_subobject, relative_path, rule_result)?; + } else { + return Err(VmError::InvalidRuleIndex { + rule_index: Value::Number(rule_idx.clone()), + }); + } + } + Value::Object(obj) => { + for (key, value) in obj.iter() { + let mut new_relative_path = relative_path.to_vec(); + new_relative_path.push(key.clone()); + self.traverse_rule_tree_subobject_with_path( + value, + result_subobject, + root_path, + &new_relative_path, + )?; + } + } + _ => {} + } + Ok(()) + } + + pub(super) fn execute_virtual_data_document_lookup(&mut self, params_index: u16) -> Result<()> { + let params = self + .program + .instruction_data + .get_virtual_data_document_lookup_params(params_index) + .ok_or(VmError::InvalidVirtualDataDocumentLookupParams { + index: params_index, + })? + .clone(); + + let mut current_node = &self.program.rule_tree["data"]; + let mut components_consumed = 0; + + for (i, component) in params.path_components.iter().enumerate() { + let key_value = match component { + LiteralOrRegister::Literal(idx) => self + .program + .literals + .get(*idx as usize) + .ok_or(VmError::LiteralIndexOutOfBounds { + index: *idx as usize, + })? + .clone(), + LiteralOrRegister::Register(reg) => self.registers[*reg as usize].clone(), + }; + + current_node = ¤t_node[&key_value]; + components_consumed = i + 1; + + match current_node { + Value::Undefined | Value::Number(_) => break, + _ => {} + } + } + + match current_node { + Value::Number(rule_index_value) => { + if let Some(rule_index) = rule_index_value.as_u64() { + let rule_index = rule_index as u16; + + self.execute_call_rule_common(params.dest, rule_index, None)?; + + if components_consumed < params.path_components.len() { + let mut rule_result = self.registers[params.dest as usize].clone(); + + for component in ¶ms.path_components[components_consumed..] { + let key_value = match component { + LiteralOrRegister::Literal(idx) => self + .program + .literals + .get(*idx as usize) + .ok_or(VmError::LiteralIndexOutOfBounds { + index: *idx as usize, + })? + .clone(), + LiteralOrRegister::Register(reg) => { + self.registers[*reg as usize].clone() + } + }; + + rule_result = rule_result[&key_value].clone(); + } + + self.registers[params.dest as usize] = rule_result; + } + } else { + return Err(VmError::InvalidRuleIndex { + rule_index: Value::Number(rule_index_value.clone()), + }); + } + } + Value::Undefined | Value::Object(_) + if components_consumed != params.path_components.len() => + { + let mut result = self.data.clone(); + + for component in ¶ms.path_components { + let key_value = match component { + LiteralOrRegister::Literal(idx) => self + .program + .literals + .get(*idx as usize) + .ok_or(VmError::LiteralIndexOutOfBounds { + index: *idx as usize, + })? + .clone(), + LiteralOrRegister::Register(reg) => self.registers[*reg as usize].clone(), + }; + + result = result[&key_value].clone(); + } + + self.registers[params.dest as usize] = result; + } + Value::Object(_) => { + let rule_tree_subobject = current_node.clone(); + + let result = self.execute_virtual_data_document_lookup_subobject( + ¶ms.path_components, + &rule_tree_subobject, + )?; + self.registers[params.dest as usize] = result; + } + _ => { + return Err(VmError::InvalidRuleTreeEntry { + value: current_node.clone(), + }); + } + } + + Ok(()) + } +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 4adb12f..cecf922 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -mod interpreter; +pub mod interpreter; mod scheduler; diff --git a/src/value.rs b/src/value.rs index 7c5ce51..e654f08 100644 --- a/src/value.rs +++ b/src/value.rs @@ -4,6 +4,7 @@ use crate::number::Number; use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::vec::Vec; use core::fmt; use core::ops; @@ -67,7 +68,7 @@ impl Serialize for Value { { use serde::ser::Error; match self { - Value::Null => serializer.serialize_none(), + Value::Null => serializer.serialize_unit(), Value::Bool(b) => serializer.serialize_bool(*b), Value::String(s) => serializer.serialize_str(s.as_ref()), Value::Number(n) => n.serialize(serializer), @@ -118,6 +119,20 @@ impl<'de> Visitor<'de> for ValueVisitor { Ok(Value::Bool(v)) } + fn visit_none(self) -> Result + where + E: de::Error, + { + Ok(Value::Null) + } + + fn visit_some(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Value::deserialize(deserializer) + } + fn visit_u64(self, v: u64) -> Result where E: de::Error, diff --git a/tests/rvm/vm/README.md b/tests/rvm/vm/README.md new file mode 100644 index 0000000..19d36bb --- /dev/null +++ b/tests/rvm/vm/README.md @@ -0,0 +1,78 @@ +# Regorus VM Test Suites + +This directory hosts YAML-driven regression suites for the Regorus virtual machine. Each YAML file is converted into parameterised Rust tests by `src/rvm/tests/vm.rs`, so the contents here define the end-to-end VM coverage. + +## Prerequisites + +- Enable the `rvm` feature (it pulls in `std` and the VM runtime) whenever you run these tests: + - `cargo test --features rvm run_vm_test_file` + - `cargo test --features rvm run_loop_test_file` +- Append `-- --nocapture` to surface per-test diagnostics when a failure occurs. +- Individual generated tests follow the pattern `run_vm_test_file_tests_rvm_vm_suites__yaml`, so you can use that fragment with `cargo test` to run a single suite. + +## Layout + +- `suites/*.yaml` — primary instruction, control-flow, and integration suites. +- `suites/loops/*.yaml` — dedicated loop/comprehension suites. +- Mirror the comment headers inside each suite when adding new files; the descriptions are surfaced in this README for quick reference. + +## Main Suites (`suites/*.yaml`) + +| Suite | Focus | +| --- | --- | +| `arithmetic_operations.yaml` | Arithmetic opcodes (`Add`, `Sub`, `Mul`, `Div`, `Mod`) and simple expressions. | +| `assertions.yaml` | `AssertCondition` semantics, including success/failure and loop control interactions. | +| `basic_instructions.yaml` | Core load/move/return instructions that underpin every program. | +| `boolean_literals.yaml` | `LoadBool`, `LoadTrue`, `LoadFalse`, and their interaction with logical operators. | +| `builtin_functions.yaml` | Builtin dispatch covering argument marshalling, return handling, and error cases. | +| `call_rule.yaml` | `CallRule` execution, rule caches, defaults, and fallbacks. | +| `comparison_operations.yaml` | Relational operators plus logical combining (`Eq`, `Ne`, `Lt`, `Le`, `Gt`, `Ge`, `And`, `Or`, `Not`). | +| `complex.yaml` | Deeply nested hybrid loops, comprehensions, and rule calls that stress the scheduler. | +| `constructed_collections.yaml` | `ArrayCreate`/`SetCreate` success paths, undefined propagation, and deduplication. | +| `control_flow.yaml` | Conditional branching patterns, nested assertions, and selection logic. | +| `core_semantics.yaml` | Broad regression coverage for arithmetic, comparisons, loops, assertions, and collection helpers. | +| `data_structures.yaml` | Array/object/set creation, access, and mutation instructions. | +| `deep_nesting.yaml` | Three-plus levels of mixed loop modes validating register pressure and control flow correctness. | +| `default_rules.yaml` | Complete rule execution with default literals and failure fallbacks. | +| `destructuring_rules.yaml` | Destructuring metadata handling, success/early-exit semantics. | +| `function_calls.yaml` | User function invocation plumbing, argument passing, and returns. | +| `halt.yaml` | `Halt` instruction returning register `0` and stopping execution. | +| `host_await.yaml` | Successful `HostAwait` responses across execution modes and run-to-completion flows. | +| `host_await_failures.yaml` | Error signalling and ignore-flag behaviour for `HostAwait`. | +| `indexed_access.yaml` | Literal/register indexing, chained accesses, and undefined propagation. | +| `integration_scenarios.yaml` | Real-world policy shapes (RBAC, filtering, transforms, workflows). | +| `interpreter_operator_compatibility.yaml` | Ensures VM operators match interpreter behaviour on edge cases. | +| `invalid_collection_ops.yaml` | Error paths for object/set/array mutations with incorrect types. | +| `load_data_input.yaml` | `LoadData` and `LoadInput` instructions across nested/empty/undefined sources. | +| `loop_invalid_iteration.yaml` | Loop errors for non-iterables plus instruction-limit enforcement. | +| `null_undefined_handling.yaml` | Null/undefined behaviour across arithmetic, comparisons, indexing, loops, and comprehensions. | +| `object_operations.yaml` | Advanced object templates, dynamic keys, collisions, and validation. | +| `predefined.yaml` | Global `data` and `input` bindings, including nested access patterns. | +| `resource_limits.yaml` | Instruction counts, recursion depth, and other resource exhaustion scenarios. | +| `serialization.yaml` | Round-trip binary serialization for compiled programs covering all instruction families. | +| `set_operations.yaml` | Set creation, deduplication, membership checks, and nested values. | +| `type_errors.yaml` | Graceful error reporting for cross-family type mismatches. | +| `virtual_data_lookup.yaml` | `VirtualDataDocumentLookup` with base data, rule overrides, and invalid indices. | + +## Loop Suites (`suites/loops/*.yaml`) + +| Suite | Focus | +| --- | --- | +| `array_comprehensions.yaml` | Mapping, filtering, and edge cases for array comprehensions. | +| `empty.yaml` | Behaviour of every loop mode over empty collections (vacuous truth/falsehood). | +| `existential.yaml` | `some`-style (`Any`) quantification including early exits and complex predicates. | +| `loop_comprehension_interactions.yaml` | Interplay between nested loops and comprehensions emitting structured data. | +| `nested.yaml` | Mixed nesting patterns for loops and comprehensions with varying depth. | +| `nested_fixed.yaml` | Placeholder for future fixed-nesting scenarios (no cases yet). | +| `object_comprehensions.yaml` | Key/value emission, collision handling, and filtering in object comprehensions. | +| `set_comprehensions.yaml` | Deduplication and uniqueness guarantees in set comprehensions. | +| `universal.yaml` | `every`-style (`Every`) quantification, early failure, and vacuous truth cases. | + +## Adding or Updating Suites + +1. Place the new YAML file under `suites/` (or the relevant `suites/loops/` subdirectory). +2. Add a concise comment block at the top describing the intent and scenarios. +3. Update the tables above so the catalog stays accurate. +4. Run `cargo test --features rvm run_vm_test_file` to ensure the suite loads and all cases pass. + +Keeping this README current makes it easier to discover coverage gaps and reason about the generated tests. diff --git a/tests/rvm/vm/suites/arithmetic_operations.yaml b/tests/rvm/vm/suites/arithmetic_operations.yaml new file mode 100644 index 0000000..b183151 --- /dev/null +++ b/tests/rvm/vm/suites/arithmetic_operations.yaml @@ -0,0 +1,168 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Arithmetic Operations Test Suite +# Tests mathematical operations: Add, Sub, Mul, Div +# These instructions perform basic arithmetic on numeric values + +cases: + - note: arithmetic_add + description: Test Add instruction + example_rego: "10 + 5" # Addition expression + literals: + - 10 + - 5 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load literal 10 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load literal 5 into register 1 + - "Add { dest: 2, left: 0, right: 1 }" # Add register 0 + register 1, store in register 2 + - "Return { value: 2 }" # Return result from register 2 + want_result: 15 + + - note: arithmetic_sub + description: Test Sub instruction + example_rego: "10 - 3" # Subtraction expression + literals: + - 10 + - 3 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load literal 10 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load literal 3 into register 1 + - "Sub { dest: 2, left: 0, right: 1 }" # Subtract register 1 from register 0, store in register 2 + - "Return { value: 2 }" # Return result from register 2 + want_result: 7 + + - note: arithmetic_mul + description: Test Mul instruction + example_rego: "4 * 6" # Multiplication expression + literals: + - 4 + - 6 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load literal 4 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load literal 6 into register 1 + - "Mul { dest: 2, left: 0, right: 1 }" # Multiply register 0 * register 1, store in register 2 + - "Return { value: 2 }" # Return result from register 2 + want_result: 24 + + - note: arithmetic_div + description: Test Div instruction + example_rego: "15 / 3" # Division expression + literals: + - 15 + - 3 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load literal 15 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load literal 3 into register 1 + - "Div { dest: 2, left: 0, right: 1 }" # Divide register 0 / register 1, store in register 2 + - "Return { value: 2 }" # Return result from register 2 + want_result: 5 + + - note: arithmetic_div_by_zero + description: Test Div by zero - undefined normally, error in strict mode + example_rego: "10 / 0" + literals: + - 10 + - 0 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Div { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" + want_error_strict: "Cannot divide" + + - note: arithmetic_mod + description: Test Mod instruction + example_rego: "10 % 3" + literals: + - 10 + - 3 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Mod { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: 1 + + - note: arithmetic_mod_by_zero + description: Test Mod by zero - undefined normally, error in strict mode + example_rego: "10 % 0" + literals: + - 10 + - 0 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Mod { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" + want_error_strict: "Cannot modulo" + + - note: arithmetic_mod_negative_operands + description: Test Mod with negative operands + example_rego: "-10 % 3" + literals: + - -10 + - 3 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Mod { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: -1 + + - note: arithmetic_mod_on_float_error + description: Test Mod on float - should error + example_rego: "10.5 % 3" + literals: + - 10.5 + - 3 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Mod { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_error: "modulo on floating-point number" + + - note: arithmetic_chained_operations + description: Test chained arithmetic operations (a + b) * c + example_rego: "(5 + 3) * 2" + literals: + - 5 + - 3 + - 2 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load 5 + - "Load { dest: 1, literal_idx: 1 }" # Load 3 + - "Add { dest: 2, left: 0, right: 1 }" # 5 + 3 = 8 + - "Load { dest: 3, literal_idx: 2 }" # Load 2 + - "Mul { dest: 4, left: 2, right: 3 }" # 8 * 2 = 16 + - "Return { value: 4 }" + want_result: 16 + + - note: arithmetic_float_precision + description: Test float arithmetic precision + example_rego: "0.1 + 0.2" + literals: + - 0.1 + - 0.2 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Add { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: 0.3 + + - note: arithmetic_large_numbers + description: Test arithmetic with large numbers + example_rego: "1000000 * 1000000" + literals: + - 1000000 + - 1000000 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Mul { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: 1000000000000 diff --git a/tests/rvm/vm/suites/assertions.yaml b/tests/rvm/vm/suites/assertions.yaml new file mode 100644 index 0000000..b616581 --- /dev/null +++ b/tests/rvm/vm/suites/assertions.yaml @@ -0,0 +1,47 @@ +# Assertions Test Suite +# Exercises AssertNotUndefined behavior in isolation and inside loops. + +cases: + - note: assert_not_undefined_passes + description: AssertNotUndefined succeeds when register has a value + literals: + - 42 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "AssertNotUndefined { register: 0 }" + - "Return { value: 0 }" + want_result: 42 + + - note: assert_not_undefined_fails + description: AssertNotUndefined triggers assertion error when register undefined + literals: + - "#undefined" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "AssertNotUndefined { register: 0 }" + - "Return { value: 0 }" + want_error: "Assertion failed" + + - note: assert_not_undefined_inside_loop + description: AssertNotUndefined fails inside Every loop and exits with false result + literals: + - "#undefined" + instruction_params: + loop_params: + - mode: "Every" + collection: 0 + key_reg: 1 + value_reg: 2 + result_reg: 3 + body_start: 5 + loop_end: 7 + instructions: + - "ArrayNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "ArrayPush { arr: 0, value: 1 }" + - "LoopStart { params_index: 0 }" + - "Load { dest: 2, literal_idx: 0 }" + - "AssertNotUndefined { register: 2 }" + - "LoopNext { body_start: 5, loop_end: 7 }" + - "Return { value: 3 }" + want_result: false diff --git a/tests/rvm/vm/suites/basic_instructions.yaml b/tests/rvm/vm/suites/basic_instructions.yaml new file mode 100644 index 0000000..15e4f63 --- /dev/null +++ b/tests/rvm/vm/suites/basic_instructions.yaml @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Basic VM Instructions Test Suite +# Tests fundamental instructions: Load, Move, Return +# These form the foundation for all other VM operations + +cases: + - note: load_instruction + description: Test basic Load instruction + example_rego: "42" # Simple literal expression + literals: + - 42 + - "hello" + - true + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load literal 42 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load literal "hello" into register 1 + - "Load { dest: 2, literal_idx: 2 }" # Load literal true into register 2 + - "Return { value: 0 }" # Return value from register 0 + want_result: 42 + + - note: move_instruction + description: Test Move instruction + example_rego: "x := 123; x" # Variable assignment and reference + literals: + - 123 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load literal 123 into register 0 (x := 123) + - "Move { dest: 1, src: 0 }" # Move value from register 0 to register 1 (reference x) + - "Return { value: 1 }" # Return value from register 1 + want_result: 123 diff --git a/tests/rvm/vm/suites/boolean_literals.yaml b/tests/rvm/vm/suites/boolean_literals.yaml new file mode 100644 index 0000000..b7cf2b2 --- /dev/null +++ b/tests/rvm/vm/suites/boolean_literals.yaml @@ -0,0 +1,44 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Boolean Literal Instructions Test Suite +# Validates that LoadBool behaves consistently with LoadTrue/LoadFalse +# and integrates correctly with logical instructions. + +cases: + - note: load_bool_true + description: LoadBool true matches LoadTrue semantics + literals: [] + instructions: + - "LoadBool { dest: 0, value: true }" + - "Return { value: 0 }" + want_result: true + + - note: load_bool_false + description: LoadBool false matches LoadFalse semantics + literals: [] + instructions: + - "LoadBool { dest: 0, value: false }" + - "Return { value: 0 }" + want_result: false + + - note: load_bool_with_and + description: Combine LoadBool values using And instruction + literals: [] + instructions: + - "LoadBool { dest: 0, value: true }" + - "LoadBool { dest: 1, value: false }" + - "And { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: false + + - note: load_bool_with_or_not + description: Ensure LoadBool integrates with Or and Not + literals: [] + instructions: + - "LoadBool { dest: 0, value: false }" + - "LoadBool { dest: 1, value: true }" + - "Or { dest: 2, left: 0, right: 1 }" + - "Not { dest: 3, operand: 2 }" + - "Return { value: 3 }" + want_result: false diff --git a/tests/rvm/vm/suites/builtin_functions.yaml b/tests/rvm/vm/suites/builtin_functions.yaml new file mode 100644 index 0000000..a4f4d57 --- /dev/null +++ b/tests/rvm/vm/suites/builtin_functions.yaml @@ -0,0 +1,524 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Builtin Functions Test Suite +# Tests the VM's builtin function call mechanism with various builtin functions +# Covers argument handling, return values, and error cases + +cases: + # Basic Count Function Tests + - note: builtin_count_array + description: Test count builtin with array argument + example_rego: "count([1, 2, 3])" + literals: + - [1, 2, 3] + instruction_params: + builtin_infos: + - name: "count" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load [1, 2, 3] into register 0 + - "BuiltinCall { params_index: 0 }" # Call count(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: 3 + + - note: builtin_count_string + description: Test count builtin with string argument + example_rego: "count(\"hello\")" + literals: + - "hello" + instruction_params: + builtin_infos: + - name: "count" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load "hello" into register 0 + - "BuiltinCall { params_index: 0 }" # Call count(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: 5 + + - note: builtin_count_object + description: Test count builtin with object argument + example_rego: "count({\"a\": 1, \"b\": 2})" + literals: + - {"a": 1, "b": 2} + instruction_params: + builtin_infos: + - name: "count" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load object into register 0 + - "BuiltinCall { params_index: 0 }" # Call count(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: 2 + + - note: builtin_count_empty_array + description: Test count builtin with empty array + example_rego: "count([])" + literals: + - [] + instruction_params: + builtin_infos: + - name: "count" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load empty array into register 0 + - "BuiltinCall { params_index: 0 }" # Call count(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: 0 + + # Max Function Tests + - note: builtin_max_array + description: Test max builtin with array argument + example_rego: "max([1, 5, 3])" + literals: + - [1, 5, 3] + instruction_params: + builtin_infos: + - name: "max" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load [1, 5, 3] into register 0 + - "BuiltinCall { params_index: 0 }" # Call max(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: 5 + + - note: builtin_max_empty_array + description: Test max builtin with empty array returns undefined + example_rego: "max([])" + literals: + - [] + instruction_params: + builtin_infos: + - name: "max" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load empty array into register 0 + - "BuiltinCall { params_index: 0 }" # Call max(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: "#undefined" + + # Min Function Tests + - note: builtin_min_array + description: Test min builtin with array argument + example_rego: "min([1, 5, 3])" + literals: + - [1, 5, 3] + instruction_params: + builtin_infos: + - name: "min" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load [1, 5, 3] into register 0 + - "BuiltinCall { params_index: 0 }" # Call min(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: 1 + + # Sum Function Tests + - note: builtin_sum_array + description: Test sum builtin with numeric array + example_rego: "sum([1, 2, 3, 4])" + literals: + - [1, 2, 3, 4] + instruction_params: + builtin_infos: + - name: "sum" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load [1, 2, 3, 4] into register 0 + - "BuiltinCall { params_index: 0 }" # Call sum(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: 10 + + - note: builtin_sum_empty_array + description: Test sum builtin with empty array + example_rego: "sum([])" + literals: + - [] + instruction_params: + builtin_infos: + - name: "sum" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load empty array into register 0 + - "BuiltinCall { params_index: 0 }" # Call sum(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: 0 + + # String Functions Tests + - note: builtin_upper_string + description: Test upper builtin function + example_rego: "upper(\"hello\")" + literals: + - "hello" + instruction_params: + builtin_infos: + - name: "upper" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load "hello" into register 0 + - "BuiltinCall { params_index: 0 }" # Call upper(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: "HELLO" + + - note: builtin_lower_string + description: Test lower builtin function + example_rego: "lower(\"WORLD\")" + literals: + - "WORLD" + instruction_params: + builtin_infos: + - name: "lower" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load "WORLD" into register 0 + - "BuiltinCall { params_index: 0 }" # Call lower(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: "world" + + # Multi-argument Builtin Tests + - note: builtin_contains_string + description: Test contains builtin with string arguments + example_rego: "contains(\"hello world\", \"world\")" + literals: + - "hello world" + - "world" + instruction_params: + builtin_infos: + - name: "contains" + num_args: 2 + builtin_call_params: + - dest: 2 + builtin_index: 0 + args: [0, 1] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load "hello world" into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load "world" into register 1 + - "BuiltinCall { params_index: 0 }" # Call contains(register[0], register[1]), result in register 2 + - "Return { value: 2 }" # Return result + want_result: true + + - note: builtin_contains_string_false + description: Test contains builtin with non-matching strings + example_rego: "contains(\"hello\", \"world\")" + literals: + - "hello" + - "world" + instruction_params: + builtin_infos: + - name: "contains" + num_args: 2 + builtin_call_params: + - dest: 2 + builtin_index: 0 + args: [0, 1] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load "hello" into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load "world" into register 1 + - "BuiltinCall { params_index: 0 }" # Call contains(register[0], register[1]), result in register 2 + - "Return { value: 2 }" # Return result + want_result: false + + # Array Function Tests + - note: builtin_sort_array + description: Test sort builtin with array + example_rego: "sort([3, 1, 4, 1, 5])" + literals: + - [3, 1, 4, 1, 5] + instruction_params: + builtin_infos: + - name: "sort" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load unsorted array into register 0 + - "BuiltinCall { params_index: 0 }" # Call sort(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: [1, 1, 3, 4, 5] + + # Type Function Tests + - note: builtin_type_string + description: Test type_name builtin with string + example_rego: "type_name(\"hello\")" + literals: + - "hello" + instruction_params: + builtin_infos: + - name: "type_name" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load "hello" into register 0 + - "BuiltinCall { params_index: 0 }" # Call type_name(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: "string" + + - note: builtin_type_number + description: Test type_name builtin with number + example_rego: "type_name(42)" + literals: + - 42 + instruction_params: + builtin_infos: + - name: "type_name" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load 42 into register 0 + - "BuiltinCall { params_index: 0 }" # Call type_name(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: "number" + + - note: builtin_type_array + description: Test type_name builtin with array + example_rego: "type_name([1, 2, 3])" + literals: + - [1, 2, 3] + instruction_params: + builtin_infos: + - name: "type_name" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load array into register 0 + - "BuiltinCall { params_index: 0 }" # Call type_name(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: "array" + + # Complex Builtin Chain Tests + - note: builtin_chained_operations + description: Test chaining multiple builtin calls + example_rego: "upper(lower(\"HELLO\"))" + literals: + - "HELLO" + instruction_params: + builtin_infos: + - name: "lower" + num_args: 1 + - name: "upper" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + - dest: 2 + builtin_index: 1 + args: [1] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load "HELLO" into register 0 + - "BuiltinCall { params_index: 0 }" # Call lower(register[0]), result in register 1 + - "BuiltinCall { params_index: 1 }" # Call upper(register[1]), result in register 2 + - "Return { value: 2 }" # Return final result + want_result: "HELLO" + + # Number Function Tests + - note: builtin_abs_positive + description: Test abs builtin with positive number + example_rego: "abs(42)" + literals: + - 42 + instruction_params: + builtin_infos: + - name: "abs" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load 42 into register 0 + - "BuiltinCall { params_index: 0 }" # Call abs(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: 42 + + - note: builtin_abs_negative + description: Test abs builtin with negative number + example_rego: "abs(-42)" + literals: + - -42 + instruction_params: + builtin_infos: + - name: "abs" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load -42 into register 0 + - "BuiltinCall { params_index: 0 }" # Call abs(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: 42 + + # Set Function Tests - Commented out due to set literal parsing issues + # TODO: Add proper set tests once set literal parsing is fixed + # - note: builtin_union_sets + # description: Test union builtin with sets + # ... + + # Error Handling Tests + - note: builtin_missing_function + description: Test calling non-existent builtin function + example_rego: "nonexistent_function(42)" + literals: + - 42 + instruction_params: + builtin_infos: + - name: "nonexistent_function" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load 42 into register 0 + - "BuiltinCall { params_index: 0 }" # Call nonexistent_function(register[0]), should error + - "Return { value: 1 }" # Return result + want_error: "Missing builtin function: nonexistent_function" + + # Test sets and union builtin + - note: "union([{1, 2}, {2, 3}]) should return {1, 2, 3}" + description: Test union builtin with set of sets + example_rego: "union([{1, 2}, {2, 3}])" + literals: + - + set!: + - + set!: + - 1 + - 2 + - + set!: + - 2 + - 3 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "BuiltinCall { params_index: 0 }" + - "Return { value: 1 }" + instruction_params: + builtin_infos: + - name: "union" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + want_result: + set!: + - 1 + - 2 + - 3 + + # Boolean Functions + - note: builtin_is_boolean_true + description: Test is_boolean builtin with true value + example_rego: "is_boolean(true)" + literals: + - true + instruction_params: + builtin_infos: + - name: "is_boolean" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load true into register 0 + - "BuiltinCall { params_index: 0 }" # Call is_boolean(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: true + + - note: builtin_is_boolean_false + description: Test is_boolean builtin with false value + example_rego: "is_boolean(false)" + literals: + - false + instruction_params: + builtin_infos: + - name: "is_boolean" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load false into register 0 + - "BuiltinCall { params_index: 0 }" # Call is_boolean(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: true + + - note: builtin_is_boolean_number + description: Test is_boolean builtin with number value (should return false) + example_rego: "is_boolean(42)" + literals: + - 42 + instruction_params: + builtin_infos: + - name: "is_boolean" + num_args: 1 + builtin_call_params: + - dest: 1 + builtin_index: 0 + args: [0] + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load 42 into register 0 + - "BuiltinCall { params_index: 0 }" # Call is_boolean(register[0]), result in register 1 + - "Return { value: 1 }" # Return result + want_result: false diff --git a/tests/rvm/vm/suites/call_rule.yaml b/tests/rvm/vm/suites/call_rule.yaml new file mode 100644 index 0000000..ded89e9 --- /dev/null +++ b/tests/rvm/vm/suites/call_rule.yaml @@ -0,0 +1,76 @@ +# CallRule Test Suite +# Validates rule caching, partial structures, defaults, and inconsistencies. + +cases: + - note: call_rule_basic_cache + description: CallRule caches results after first evaluation + literals: + - {} + - 1 + rule_infos: + - rule_type: Complete + definitions: + - [3] + rule_tree: + data: + test: + allow: 0 + instructions: + - "CallRule { dest: 0, rule_index: 0 }" + - "CallRule { dest: 2, rule_index: 0 }" + - "Return { value: 2 }" + - "RuleInit { result_reg: 1, rule_index: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "RuleReturn {}" + want_result: 1 + + - note: call_rule_partial_object_default + description: Partial object rule returns object even when no fields defined + literals: + - {} + rule_infos: + - rule_type: PartialObject + definitions: + - [2] + instructions: + - "CallRule { dest: 0, rule_index: 0 }" + - "Return { value: 0 }" + - "RuleInit { result_reg: 1, rule_index: 0 }" + - "RuleReturn {}" + want_result: {} + + - note: call_rule_default_literal + description: Default literal used when complete rule returns undefined + literals: + - "default" + rule_infos: + - rule_type: Complete + definitions: + - [2] + default_literal_index: 0 + instructions: + - "CallRule { dest: 0, rule_index: 0 }" + - "Return { value: 0 }" + - "RuleInit { result_reg: 1, rule_index: 0 }" + - "RuleReturn {}" + want_result: "default" + + - note: call_rule_inconsistency + description: Complete rule with inconsistent results returns undefined + literals: + - 1 + - 2 + rule_infos: + - rule_type: Complete + definitions: + - [2, 5] + instructions: + - "CallRule { dest: 0, rule_index: 0 }" + - "Return { value: 0 }" + - "RuleInit { result_reg: 1, rule_index: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "RuleReturn {}" + - "RuleInit { result_reg: 1, rule_index: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "RuleReturn {}" + want_result: "#undefined" diff --git a/tests/rvm/vm/suites/comparison_operations.yaml b/tests/rvm/vm/suites/comparison_operations.yaml new file mode 100644 index 0000000..20915f9 --- /dev/null +++ b/tests/rvm/vm/suites/comparison_operations.yaml @@ -0,0 +1,111 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Comparison Operations Test Suite +# Tests comparison instructions: Eq, Ne, Lt, Le, Gt, Ge +# These instructions compare values and produce boolean results + +cases: + - note: comparison_eq + description: Test Eq instruction + example_rego: "5 == 5" # Equality comparison + literals: + - 5 + - 5 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load literal 5 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load literal 5 into register 1 + - "Eq { dest: 2, left: 0, right: 1 }" # Compare register 0 == register 1, store result in register 2 + - "Return { value: 2 }" # Return boolean result from register 2 + want_result: true + + - note: comparison_eq_false + description: Test Eq instruction with false result + example_rego: "5 == 3" # Equality comparison (false case) + literals: + - 5 + - 3 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load literal 5 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load literal 3 into register 1 + - "Eq { dest: 2, left: 0, right: 1 }" # Compare register 0 == register 1, store result in register 2 + - "Return { value: 2 }" # Return boolean result from register 2 + want_result: false + + - note: comparison_ne + description: Test Ne instruction + example_rego: "5 != 3" # Inequality comparison + literals: + - 5 + - 3 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load literal 5 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load literal 3 into register 1 + - "Ne { dest: 2, left: 0, right: 1 }" # Compare register 0 != register 1, store result in register 2 + - "Return { value: 2 }" # Return boolean result from register 2 + want_result: true + + - note: comparison_lt + description: Test Lt instruction + example_rego: "3 < 5" # Less than comparison + literals: + - 3 + - 5 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load literal 3 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load literal 5 into register 1 + - "Lt { dest: 2, left: 0, right: 1 }" # Compare register 0 < register 1, store result in register 2 + - "Return { value: 2 }" # Return boolean result from register 2 + want_result: true + + - note: comparison_le + description: Test Le instruction (less than or equal) + example_rego: "5 <= 10" # Less than or equal comparison + literals: + - 5 + - 10 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load literal 5 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load literal 10 into register 1 + - "Le { dest: 2, left: 0, right: 1 }" # Compare register 0 <= register 1, store result in register 2 + - "Return { value: 2 }" # Return boolean result from register 2 + want_result: true + + - note: comparison_ge + description: Test Ge instruction (greater than or equal) + example_rego: "10 >= 5" # Greater than or equal comparison + literals: + - 10 + - 5 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load literal 10 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load literal 5 into register 1 + - "Ge { dest: 2, left: 0, right: 1 }" # Compare register 0 >= register 1, store result in register 2 + - "Return { value: 2 }" # Return boolean result from register 2 + want_result: true + + - note: comparison_gt + description: Test Gt instruction + example_rego: "7 > 3" # Greater than comparison + literals: + - 7 + - 3 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load literal 7 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load literal 3 into register 1 + - "Gt { dest: 2, left: 0, right: 1 }" # Compare register 0 > register 1, store result in register 2 + - "Return { value: 2 }" # Return boolean result from register 2 + want_result: true + + - note: comparison_ge_equal + description: Test Ge instruction with equal values + example_rego: "5 >= 5" # Greater than or equal comparison (equal case) + literals: + - 5 + - 5 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load literal 5 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load literal 5 into register 1 + - "Ge { dest: 2, left: 0, right: 1 }" # Compare register 0 >= register 1, store result in register 2 + - "Return { value: 2 }" # Return boolean result from register 2 + want_result: true diff --git a/tests/rvm/vm/suites/complex.yaml b/tests/rvm/vm/suites/complex.yaml new file mode 100644 index 0000000..3f619ad --- /dev/null +++ b/tests/rvm/vm/suites/complex.yaml @@ -0,0 +1,230 @@ +# Complex VM Control Flow Suite +# Exercises deep nesting of heterogeneous loops, comprehensions, and rule calls. + +cases: + - note: twenty_level_loop_maze + description: Twenty levels of mixed loop modes with embedded comprehension and rule call + literals: + - 0 + - 1 + instruction_params: + loop_params: + - mode: "ForEach" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 10 + body_start: 5 + loop_end: 55 + - mode: "Any" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 11 + body_start: 6 + loop_end: 54 + - mode: "Every" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 12 + body_start: 7 + loop_end: 53 + - mode: "ForEach" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 13 + body_start: 8 + loop_end: 52 + - mode: "Any" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 14 + body_start: 9 + loop_end: 51 + - mode: "Every" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 15 + body_start: 10 + loop_end: 50 + - mode: "ForEach" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 16 + body_start: 11 + loop_end: 49 + - mode: "Any" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 17 + body_start: 12 + loop_end: 48 + - mode: "Every" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 18 + body_start: 13 + loop_end: 47 + - mode: "ForEach" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 19 + body_start: 14 + loop_end: 46 + - mode: "Any" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 20 + body_start: 15 + loop_end: 45 + - mode: "Every" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 21 + body_start: 16 + loop_end: 44 + - mode: "ForEach" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 22 + body_start: 17 + loop_end: 43 + - mode: "Any" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 23 + body_start: 18 + loop_end: 42 + - mode: "Every" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 24 + body_start: 19 + loop_end: 41 + - mode: "ForEach" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 25 + body_start: 20 + loop_end: 40 + - mode: "Any" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 26 + body_start: 21 + loop_end: 39 + - mode: "Every" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 27 + body_start: 22 + loop_end: 38 + - mode: "ForEach" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 28 + body_start: 23 + loop_end: 37 + - mode: "Any" + collection: 1 + key_reg: 2 + value_reg: 3 + result_reg: 29 + body_start: 24 + loop_end: 36 + - mode: "ForEach" + collection: 4 + key_reg: 30 + value_reg: 31 + result_reg: 32 + body_start: 28 + loop_end: 31 + comprehension_begin_params: + - mode: "Array" + collection_reg: 4 + key_reg: 30 + value_reg: 31 + body_start: 28 + comprehension_end: 31 + rule_infos: + - rule_type: Complete + definitions: + - [56] + instructions: + - "ArrayNew { dest: 1 }" + - "Load { dest: 8, literal_idx: 1 }" + - "ArrayPush { arr: 1, value: 8 }" + - "Load { dest: 7, literal_idx: 0 }" + - "LoopStart { params_index: 0 }" + - "LoopStart { params_index: 1 }" + - "LoopStart { params_index: 2 }" + - "LoopStart { params_index: 3 }" + - "LoopStart { params_index: 4 }" + - "LoopStart { params_index: 5 }" + - "LoopStart { params_index: 6 }" + - "LoopStart { params_index: 7 }" + - "LoopStart { params_index: 8 }" + - "LoopStart { params_index: 9 }" + - "LoopStart { params_index: 10 }" + - "LoopStart { params_index: 11 }" + - "LoopStart { params_index: 12 }" + - "LoopStart { params_index: 13 }" + - "LoopStart { params_index: 14 }" + - "LoopStart { params_index: 15 }" + - "LoopStart { params_index: 16 }" + - "LoopStart { params_index: 17 }" + - "LoopStart { params_index: 18 }" + - "LoopStart { params_index: 19 }" + - "ArrayNew { dest: 4 }" + - "ArrayPush { arr: 4, value: 8 }" + - "ComprehensionBegin { params_index: 0 }" + - "LoopStart { params_index: 20 }" + - "Move { dest: 31, src: 8 }" + - "ComprehensionYield { value_reg: 31 }" + - "LoopNext { body_start: 28, loop_end: 31 }" + - "ComprehensionEnd" + - "Add { dest: 7, left: 7, right: 8 }" + - "CallRule { dest: 9, rule_index: 0 }" + - "Add { dest: 7, left: 7, right: 9 }" + - "LoopNext { body_start: 24, loop_end: 36 }" + - "LoopNext { body_start: 23, loop_end: 37 }" + - "LoopNext { body_start: 22, loop_end: 38 }" + - "LoopNext { body_start: 21, loop_end: 39 }" + - "LoopNext { body_start: 20, loop_end: 40 }" + - "LoopNext { body_start: 19, loop_end: 41 }" + - "LoopNext { body_start: 18, loop_end: 42 }" + - "LoopNext { body_start: 17, loop_end: 43 }" + - "LoopNext { body_start: 16, loop_end: 44 }" + - "LoopNext { body_start: 15, loop_end: 45 }" + - "LoopNext { body_start: 14, loop_end: 46 }" + - "LoopNext { body_start: 13, loop_end: 47 }" + - "LoopNext { body_start: 12, loop_end: 48 }" + - "LoopNext { body_start: 11, loop_end: 49 }" + - "LoopNext { body_start: 10, loop_end: 50 }" + - "LoopNext { body_start: 9, loop_end: 51 }" + - "LoopNext { body_start: 8, loop_end: 52 }" + - "LoopNext { body_start: 7, loop_end: 53 }" + - "LoopNext { body_start: 6, loop_end: 54 }" + - "LoopNext { body_start: 5, loop_end: 55 }" + - "Return { value: 7 }" + - "RuleInit { result_reg: 1, rule_index: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "RuleReturn {}" + want_result: 2 diff --git a/tests/rvm/vm/suites/constructed_collections.yaml b/tests/rvm/vm/suites/constructed_collections.yaml new file mode 100644 index 0000000..f47515f --- /dev/null +++ b/tests/rvm/vm/suites/constructed_collections.yaml @@ -0,0 +1,80 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Constructed Collections Test Suite +# Validates ArrayCreate and SetCreate behavior including undefined handling. + +cases: + - note: array_create_basic + description: ArrayCreate assembles elements from registers + literals: + - 1 + - 2 + instruction_params: + array_create_params: + - dest: 3 + elements: [0, 1, 2] + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Add { dest: 2, left: 0, right: 1 }" + - "ArrayCreate { params_index: 0 }" + - "Return { value: 3 }" + want_result: + - 1 + - 2 + - 3 + + - note: array_create_with_undefined + description: ArrayCreate returns undefined if any source is undefined + literals: + - 1 + - {} + - "missing" + instruction_params: + array_create_params: + - dest: 2 + elements: [0, 1] + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 3, literal_idx: 1 }" + - "Load { dest: 4, literal_idx: 2 }" + - "Index { dest: 1, container: 3, key: 4 }" + - "ArrayCreate { params_index: 0 }" + - "Return { value: 2 }" + want_result: "#undefined" + + - note: set_create_basic + description: SetCreate deduplicates values and collects registers + literals: + - 1 + - 2 + instruction_params: + set_create_params: + - dest: 3 + elements: [0, 1, 2] + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Load { dest: 2, literal_idx: 0 }" + - "SetCreate { params_index: 0 }" + - "Return { value: 3 }" + want_result: + set!: [1, 2] + + - note: set_create_with_undefined + description: SetCreate returns undefined if any source register is undefined + literals: + - {} + - "missing" + instruction_params: + set_create_params: + - dest: 3 + elements: [0] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Index { dest: 0, container: 1, key: 2 }" + - "SetCreate { params_index: 0 }" + - "Return { value: 3 }" + want_result: "#undefined" diff --git a/tests/rvm/vm/suites/control_flow.yaml b/tests/rvm/vm/suites/control_flow.yaml new file mode 100644 index 0000000..4d8eb51 --- /dev/null +++ b/tests/rvm/vm/suites/control_flow.yaml @@ -0,0 +1,115 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Control Flow Test Suite +# Tests conditional execution and branching patterns +# Focuses on AssertCondition and conditional logic without loops + +cases: + - note: assert_condition_true + description: Test AssertCondition with true condition + example_rego: "x > 5; x := 10" # Simple condition that should succeed + literals: + - 10 + - 5 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load 10 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load 5 into register 1 + - "Gt { dest: 2, left: 0, right: 1 }" # Check if 10 > 5, store result in register 2 + - "AssertCondition { condition: 2 }" # Assert the condition (should succeed) + - "Return { value: 0 }" # Return the original value + want_result: 10 + + - note: assert_condition_false + description: Test AssertCondition with false condition + example_rego: "x > 15; x := 10" # Simple condition that should fail + literals: + - 10 + - 15 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load 10 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load 15 into register 1 + - "Gt { dest: 2, left: 0, right: 1 }" # Check if 10 > 15, store result in register 2 + - "AssertCondition { condition: 2 }" # Assert the condition (should fail) + - "Return { value: 0 }" # Return the original value (never reached) + want_result: "#undefined" # VM should return undefined for failed assertion + + - note: complex_condition_and + description: Test complex AND condition + example_rego: "x > 5; x < 15; x := 10" # Multiple conditions (both must be true) + literals: + - 10 + - 5 + - 15 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load 10 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load 5 into register 1 + - "Load { dest: 2, literal_idx: 2 }" # Load 15 into register 2 + - "Gt { dest: 3, left: 0, right: 1 }" # Check if 10 > 5, store result in register 3 + - "Lt { dest: 4, left: 0, right: 2 }" # Check if 10 < 15, store result in register 4 + - "And { dest: 5, left: 3, right: 4 }" # AND both conditions, store result in register 5 + - "AssertCondition { condition: 5 }" # Assert the combined condition (should succeed) + - "Return { value: 0 }" # Return the original value + want_result: 10 + + - note: complex_condition_or + description: Test complex OR condition + example_rego: "x < 5; x > 15; x := 10" # Either condition can be true (both are false here) + literals: + - 10 + - 5 + - 15 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load 10 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load 5 into register 1 + - "Load { dest: 2, literal_idx: 2 }" # Load 15 into register 2 + - "Lt { dest: 3, left: 0, right: 1 }" # Check if 10 < 5, store result in register 3 + - "Gt { dest: 4, left: 0, right: 2 }" # Check if 10 > 15, store result in register 4 + - "Or { dest: 5, left: 3, right: 4 }" # OR both conditions, store result in register 5 + - "AssertCondition { condition: 5 }" # Assert the combined condition (should fail) + - "Return { value: 0 }" # Return the original value (never reached) + want_result: "#undefined" # VM should return undefined for failed assertion + + - note: conditional_value_selection + description: Test conditional value selection + example_rego: "result := x > 10 ? x * 2 : x * 3; x := 15" # Conditional expression simulation + literals: + - 15 + - 10 + - 2 + - 3 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load 15 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load 10 into register 1 + - "Load { dest: 2, literal_idx: 2 }" # Load 2 into register 2 + - "Load { dest: 3, literal_idx: 3 }" # Load 3 into register 3 + - "Gt { dest: 4, left: 0, right: 1 }" # Check if 15 > 10, store result in register 4 + - "Mul { dest: 5, left: 0, right: 2 }" # Compute 15 * 2, store in register 5 + - "Mul { dest: 6, left: 0, right: 3 }" # Compute 15 * 3, store in register 6 + # Simulate conditional selection (in real VM this would use conditional instructions) + - "AssertCondition { condition: 4 }" # Since condition is true, we proceed with first value + - "Return { value: 5 }" # Return x * 2 (30) + want_result: 30 + + - note: nested_conditions + description: Test nested conditional logic + example_rego: "x > 0; y > 0; z := x + y; z > 10; x := 8; y := 5" # Nested conditions with intermediate calculation + literals: + - 8 + - 5 + - 0 + - 10 + instructions: + - "Load { dest: 0, literal_idx: 0 }" # Load x=8 into register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load y=5 into register 1 + - "Load { dest: 2, literal_idx: 2 }" # Load 0 into register 2 + - "Load { dest: 3, literal_idx: 3 }" # Load 10 into register 3 + - "Gt { dest: 4, left: 0, right: 2 }" # Check if x > 0, store result in register 4 + - "AssertCondition { condition: 4 }" # Assert x > 0 (should succeed) + - "Gt { dest: 5, left: 1, right: 2 }" # Check if y > 0, store result in register 5 + - "AssertCondition { condition: 5 }" # Assert y > 0 (should succeed) + - "Add { dest: 6, left: 0, right: 1 }" # Compute z = x + y, store in register 6 + - "Gt { dest: 7, left: 6, right: 3 }" # Check if z > 10, store result in register 7 + - "AssertCondition { condition: 7 }" # Assert z > 10 (should succeed: 13 > 10) + - "Return { value: 6 }" # Return z value (13) + want_result: 13 diff --git a/tests/rvm/vm/suites/core_semantics.yaml b/tests/rvm/vm/suites/core_semantics.yaml new file mode 100644 index 0000000..4070455 --- /dev/null +++ b/tests/rvm/vm/suites/core_semantics.yaml @@ -0,0 +1,232 @@ +# Comprehensive VM semantics regression suite +# Covers arithmetic, comparisons, loop modes, assertions, and collection helpers. + +cases: + - note: arithmetic_ops + description: Verify primitive arithmetic instructions and array aggregation + literals: + - 8 + - 2 + instructions: + - "ArrayNew { dest: 5 }" + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Add { dest: 2, left: 0, right: 1 }" + - "ArrayPush { arr: 5, value: 2 }" + - "Sub { dest: 3, left: 0, right: 1 }" + - "ArrayPush { arr: 5, value: 3 }" + - "Mul { dest: 4, left: 3, right: 1 }" + - "ArrayPush { arr: 5, value: 4 }" + - "Div { dest: 6, left: 0, right: 1 }" + - "ArrayPush { arr: 5, value: 6 }" + - "Mod { dest: 7, left: 0, right: 1 }" + - "ArrayPush { arr: 5, value: 7 }" + - "Return { value: 5 }" + want_result: [10, 6, 12, 4, 0] + + - note: comparison_and_logic + description: Validate comparison, boolean, and negation instructions + literals: + - 5 + - 5 + - 3 + instructions: + - "ArrayNew { dest: 11 }" + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Load { dest: 2, literal_idx: 2 }" + - "Eq { dest: 3, left: 0, right: 1 }" + - "ArrayPush { arr: 11, value: 3 }" + - "Ne { dest: 4, left: 0, right: 1 }" + - "ArrayPush { arr: 11, value: 4 }" + - "Lt { dest: 5, left: 2, right: 0 }" + - "ArrayPush { arr: 11, value: 5 }" + - "Ge { dest: 6, left: 0, right: 2 }" + - "ArrayPush { arr: 11, value: 6 }" + - "And { dest: 7, left: 3, right: 5 }" + - "ArrayPush { arr: 11, value: 7 }" + - "Not { dest: 8, operand: 4 }" + - "ArrayPush { arr: 11, value: 8 }" + - "Or { dest: 9, left: 4, right: 5 }" + - "ArrayPush { arr: 11, value: 9 }" + - "Return { value: 11 }" + want_result: [true, false, true, true, true, true, true] + + - note: loop_any_short_circuit_success + description: LoopMode::Any should exit on first successful iteration + literals: + - 1 + - 2 + - 3 + instruction_params: + loop_params: + - mode: "Any" + collection: 0 + key_reg: 6 + value_reg: 7 + result_reg: 8 + body_start: 9 + loop_end: 12 + instructions: + - "ArrayNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "ArrayPush { arr: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "ArrayPush { arr: 0, value: 2 }" + - "Load { dest: 3, literal_idx: 2 }" + - "ArrayPush { arr: 0, value: 3 }" + - "Load { dest: 4, literal_idx: 1 }" + - "LoopStart { params_index: 0 }" + - "Eq { dest: 5, left: 7, right: 4 }" + - "AssertCondition { condition: 5 }" + - "LoopNext { body_start: 9, loop_end: 11 }" + - "Return { value: 8 }" + want_result: true + + - note: loop_any_all_fail + description: LoopMode::Any should remain false when no iteration succeeds + literals: + - 1 + - 2 + - 3 + - 99 + instruction_params: + loop_params: + - mode: "Any" + collection: 0 + key_reg: 6 + value_reg: 7 + result_reg: 8 + body_start: 9 + loop_end: 12 + instructions: + - "ArrayNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "ArrayPush { arr: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "ArrayPush { arr: 0, value: 2 }" + - "Load { dest: 3, literal_idx: 2 }" + - "ArrayPush { arr: 0, value: 3 }" + - "Load { dest: 4, literal_idx: 3 }" + - "LoopStart { params_index: 0 }" + - "Eq { dest: 5, left: 7, right: 4 }" + - "AssertCondition { condition: 5 }" + - "LoopNext { body_start: 9, loop_end: 11 }" + - "Return { value: 8 }" + want_result: false + + - note: loop_every_exits_on_failure + description: LoopMode::Every stops at the first failed iteration and returns false + literals: + - 1 + - 0 + - 1 + instruction_params: + loop_params: + - mode: "Every" + collection: 0 + key_reg: 6 + value_reg: 7 + result_reg: 8 + body_start: 10 + loop_end: 14 + instructions: + - "ArrayNew { dest: 0 }" + - "ArrayNew { dest: 9 }" + - "Load { dest: 1, literal_idx: 0 }" + - "ArrayPush { arr: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "ArrayPush { arr: 0, value: 2 }" + - "Load { dest: 3, literal_idx: 2 }" + - "ArrayPush { arr: 0, value: 3 }" + - "Load { dest: 4, literal_idx: 0 }" + - "LoopStart { params_index: 0 }" + - "ArrayPush { arr: 9, value: 7 }" + - "Eq { dest: 5, left: 7, right: 4 }" + - "AssertCondition { condition: 5 }" + - "LoopNext { body_start: 10, loop_end: 13 }" + - "ArrayNew { dest: 10 }" + - "ArrayPush { arr: 10, value: 8 }" + - "ArrayPush { arr: 10, value: 9 }" + - "Return { value: 10 }" + want_result: + - false + - [1, 0] + + - note: loop_foreach_collects_successes + description: LoopMode::ForEach tracks successful iterations while visiting all elements + literals: + - 0 + - 5 + instruction_params: + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 6 + value_reg: 7 + result_reg: 8 + body_start: 9 + loop_end: 13 + instructions: + - "ArrayNew { dest: 0 }" + - "ArrayNew { dest: 9 }" + - "Load { dest: 1, literal_idx: 0 }" + - "ArrayPush { arr: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 0 }" + - "ArrayPush { arr: 0, value: 2 }" + - "Load { dest: 3, literal_idx: 1 }" + - "ArrayPush { arr: 0, value: 3 }" + - "LoopStart { params_index: 0 }" + - "Eq { dest: 4, left: 7, right: 3 }" + - "ArrayPush { arr: 9, value: 4 }" + - "AssertCondition { condition: 4 }" + - "LoopNext { body_start: 9, loop_end: 12 }" + - "ArrayNew { dest: 10 }" + - "ArrayPush { arr: 10, value: 8 }" + - "ArrayPush { arr: 10, value: 9 }" + - "Return { value: 10 }" + want_result: + - true + - [false, false, true] + + - note: assert_condition_without_loop_errors + description: AssertCondition outside of a loop reports an assertion failure + literals: + - false + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "AssertCondition { condition: 0 }" + - "Return { value: 0 }" + want_error: "Assertion failed" + + - note: contains_and_count_helpers + description: Contains and Count semantics across arrays, sets, and non-collections + literals: + - 1 + - 2 + - 9 + instructions: + - "ArrayNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "ArrayPush { arr: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "ArrayPush { arr: 0, value: 2 }" + - "SetNew { dest: 3 }" + - "SetAdd { set: 3, value: 1 }" + - "SetAdd { set: 3, value: 2 }" + - "Contains { dest: 4, collection: 0, value: 1 }" + - "Contains { dest: 5, collection: 3, value: 2 }" + - "Load { dest: 6, literal_idx: 2 }" + - "Contains { dest: 7, collection: 3, value: 6 }" + - "Count { dest: 8, collection: 0 }" + - "Count { dest: 9, collection: 3 }" + - "Count { dest: 10, collection: 1 }" + - "ArrayNew { dest: 11 }" + - "ArrayPush { arr: 11, value: 4 }" + - "ArrayPush { arr: 11, value: 5 }" + - "ArrayPush { arr: 11, value: 7 }" + - "ArrayPush { arr: 11, value: 8 }" + - "ArrayPush { arr: 11, value: 9 }" + - "ArrayPush { arr: 11, value: 10 }" + - "Return { value: 11 }" + want_result: [true, true, false, 2, 2, "#undefined"] diff --git a/tests/rvm/vm/suites/data_structures.yaml b/tests/rvm/vm/suites/data_structures.yaml new file mode 100644 index 0000000..28f2b3c --- /dev/null +++ b/tests/rvm/vm/suites/data_structures.yaml @@ -0,0 +1,76 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Data Structures Test Suite +# Tests instructions for working with arrays, objects, and sets +# These instructions create and manipulate complex data types + +cases: + - note: array_operations + description: Test array creation and access + example_rego: "arr := [1, 2, 3]; arr[0]" # Array creation and indexing + literals: + - 1 + - 2 + - 3 + - 0 # index + instructions: + - "ArrayNew { dest: 0 }" # Create empty array in register 0 + - "Load { dest: 1, literal_idx: 0 }" # Load 1 into register 1 + - "ArrayPush { arr: 0, value: 1 }" # Push 1 to array + - "Load { dest: 2, literal_idx: 1 }" # Load 2 into register 2 + - "ArrayPush { arr: 0, value: 2 }" # Push 2 to array + - "Load { dest: 3, literal_idx: 2 }" # Load 3 into register 3 + - "ArrayPush { arr: 0, value: 3 }" # Push 3 to array + - "Load { dest: 4, literal_idx: 3 }" # Load index 0 into register 4 + - "Index { dest: 5, container: 0, key: 4 }" # Access array[0], store in register 5 + - "Return { value: 5 }" # Return the indexed value + want_result: 1 + + - note: object_operations + description: Test object creation and access + example_rego: "obj := {\"key1\": \"value1\", \"key2\": 42}; obj.key1" # Object creation and field access + literals: + - {} + - "key1" + - "value1" + - "key2" + - 42 + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + instructions: + - "ObjectCreate { params_index: 0 }" # Create empty object in register 0 + - "Load { dest: 1, literal_idx: 1 }" # Load "key1" into register 1 + - "Load { dest: 2, literal_idx: 2 }" # Load "value1" into register 2 + - "ObjectSet { obj: 0, key: 1, value: 2 }" # Set obj["key1"] = "value1" + - "Load { dest: 3, literal_idx: 3 }" # Load "key2" into register 3 + - "Load { dest: 4, literal_idx: 4 }" # Load 42 into register 4 + - "ObjectSet { obj: 0, key: 3, value: 4 }" # Set obj["key2"] = 42 + - "Load { dest: 5, literal_idx: 1 }" # Load "key1" again for lookup + - "Index { dest: 6, container: 0, key: 5 }" # Access obj["key1"], store in register 6 + - "Return { value: 6 }" # Return the field value + want_result: "value1" + + - note: set_operations + description: Test set creation and membership + example_rego: "s := {1, 2, 3}; 2 in s" # Set creation and membership test + literals: + - 1 + - 2 + - 3 + instructions: + - "SetNew { dest: 0 }" # Create empty set in register 0 + - "Load { dest: 1, literal_idx: 0 }" # Load 1 into register 1 + - "SetAdd { set: 0, value: 1 }" # Add 1 to set + - "Load { dest: 2, literal_idx: 1 }" # Load 2 into register 2 + - "SetAdd { set: 0, value: 2 }" # Add 2 to set + - "Load { dest: 3, literal_idx: 2 }" # Load 3 into register 3 + - "SetAdd { set: 0, value: 3 }" # Add 3 to set + - "Load { dest: 4, literal_idx: 1 }" # Load 2 again for membership check + - "Contains { dest: 5, collection: 0, value: 4 }" # Check if 2 is in set, store result in register 5 + - "Return { value: 5 }" # Return boolean membership result + want_result: true diff --git a/tests/rvm/vm/suites/deep_nesting.yaml b/tests/rvm/vm/suites/deep_nesting.yaml new file mode 100644 index 0000000..c63fea1 --- /dev/null +++ b/tests/rvm/vm/suites/deep_nesting.yaml @@ -0,0 +1,278 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Deep Nesting Test Suite +# Tests 3+ levels of nested loops with various loop modes and comprehensions +# Verifies register allocation, stack pressure, and control flow correctness + +cases: + - note: three_level_nested_any_every_foreach + description: 3-level nesting - Any inside Every inside ForEach + example_rego: | + [outer | + outer := [1, 2][_]; + every mid in [1, 2]; some inner in [1, 2]; inner == mid + ] + literals: + - 1 + - 2 + instruction_params: + comprehension_begin_params: + - mode: "Array" + collection_reg: 0 + result_reg: 0 + key_reg: 10 + value_reg: 11 + body_start: 7 + comprehension_end: 27 + loop_params: + - mode: "ForEach" # Outer loop for comprehension + collection: 5 + key_reg: 10 + value_reg: 11 + result_reg: 12 + body_start: 7 + loop_end: 27 + - mode: "Every" # Middle loop + collection: 8 + key_reg: 20 + value_reg: 21 + result_reg: 22 + body_start: 13 + loop_end: 24 + - mode: "Any" # Inner loop + collection: 13 + key_reg: 30 + value_reg: 31 + result_reg: 32 + body_start: 19 + loop_end: 22 + instructions: + # Build outer collection [1, 2] + - "ArrayNew { dest: 5 }" + - "Load { dest: 1, literal_idx: 0 }" + - "ArrayPush { arr: 5, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "ArrayPush { arr: 5, value: 2 }" + - "ComprehensionBegin { params_index: 0 }" + - "LoopStart { params_index: 0 }" # ForEach outer + # Build middle collection [1, 2] + - "ArrayNew { dest: 8 }" + - "Load { dest: 3, literal_idx: 0 }" + - "ArrayPush { arr: 8, value: 3 }" + - "Load { dest: 4, literal_idx: 1 }" + - "ArrayPush { arr: 8, value: 4 }" + - "LoopStart { params_index: 1 }" # Every middle + # Build inner collection [1, 2] + - "ArrayNew { dest: 13 }" + - "Load { dest: 6, literal_idx: 0 }" + - "ArrayPush { arr: 13, value: 6 }" + - "Load { dest: 7, literal_idx: 1 }" + - "ArrayPush { arr: 13, value: 7 }" + - "LoopStart { params_index: 2 }" # Any inner + # Check condition: inner == mid + - "Eq { dest: 40, left: 31, right: 21 }" + - "AssertCondition { condition: 40 }" + - "LoopNext { body_start: 19, loop_end: 22 }" + # End Any loop - check result + - "AssertCondition { condition: 32 }" + - "LoopNext { body_start: 13, loop_end: 24 }" + # End Every loop - check result + - "AssertCondition { condition: 22 }" + - "ComprehensionYield { value_reg: 11 }" + - "LoopNext { body_start: 7, loop_end: 27 }" + - "ComprehensionEnd" + - "Return { value: 0 }" + want_result: [1, 2] + + - note: four_level_nested_loops + description: 4-level nested loops - stress test for stack depth + example_rego: | + some a in [1]; some b in [2]; some c in [3]; some d in [4]; a + b + c + d == 10 + literals: + - 1 + - 2 + - 3 + - 4 + - 10 + instruction_params: + loop_params: + - mode: "Any" + collection: 0 + key_reg: 10 + value_reg: 11 + result_reg: 12 + body_start: 4 + loop_end: 29 + - mode: "Any" + collection: 2 + key_reg: 20 + value_reg: 21 + result_reg: 22 + body_start: 8 + loop_end: 27 + - mode: "Any" + collection: 4 + key_reg: 30 + value_reg: 31 + result_reg: 32 + body_start: 12 + loop_end: 25 + - mode: "Any" + collection: 6 + key_reg: 40 + value_reg: 41 + result_reg: 42 + body_start: 16 + loop_end: 23 + instructions: + - "ArrayNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "ArrayPush { arr: 0, value: 1 }" + - "LoopStart { params_index: 0 }" # Level 1 + - "ArrayNew { dest: 2 }" + - "Load { dest: 3, literal_idx: 1 }" + - "ArrayPush { arr: 2, value: 3 }" + - "LoopStart { params_index: 1 }" # Level 2 + - "ArrayNew { dest: 4 }" + - "Load { dest: 5, literal_idx: 2 }" + - "ArrayPush { arr: 4, value: 5 }" + - "LoopStart { params_index: 2 }" # Level 3 + - "ArrayNew { dest: 6 }" + - "Load { dest: 7, literal_idx: 3 }" + - "ArrayPush { arr: 6, value: 7 }" + - "LoopStart { params_index: 3 }" # Level 4 + # Compute sum: a + b + c + d + - "Add { dest: 43, left: 11, right: 21 }" + - "Add { dest: 44, left: 43, right: 31 }" + - "Add { dest: 45, left: 44, right: 41 }" + - "Load { dest: 46, literal_idx: 4 }" + - "Eq { dest: 47, left: 45, right: 46 }" + - "AssertCondition { condition: 47 }" + - "LoopNext { body_start: 16, loop_end: 23 }" + - "AssertCondition { condition: 42 }" + - "LoopNext { body_start: 12, loop_end: 25 }" + - "AssertCondition { condition: 32 }" + - "LoopNext { body_start: 8, loop_end: 27 }" + - "AssertCondition { condition: 22 }" + - "LoopNext { body_start: 4, loop_end: 29 }" + - "Return { value: 12 }" + want_result: true + + - note: comprehension_inside_nested_loop + description: Array comprehension inside 2-level nested loop + example_rego: | + [[y | y := inner[_]] | + outer := [[1, 2], [3, 4]][_]; + inner := outer + ] + literals: + - 1 + - 2 + - 3 + - 4 + instruction_params: + comprehension_begin_params: + - mode: "Array" # Outer comprehension + collection_reg: 0 + key_reg: 10 + value_reg: 11 + body_start: 15 + comprehension_end: 23 + - mode: "Array" # Inner comprehension + collection_reg: 14 + key_reg: 20 + value_reg: 21 + body_start: 18 + comprehension_end: 20 + loop_params: + - mode: "ForEach" # Outer loop + collection: 8 + key_reg: 10 + value_reg: 11 + result_reg: 13 + body_start: 15 + loop_end: 23 + - mode: "ForEach" # Inner loop for comprehension + collection: 12 + key_reg: 20 + value_reg: 21 + result_reg: 22 + body_start: 18 + loop_end: 20 + instructions: + # Build outer array [[1, 2], [3, 4]] + - "ArrayNew { dest: 8 }" + - "ArrayNew { dest: 1 }" + - "Load { dest: 2, literal_idx: 0 }" + - "ArrayPush { arr: 1, value: 2 }" + - "Load { dest: 3, literal_idx: 1 }" + - "ArrayPush { arr: 1, value: 3 }" + - "ArrayPush { arr: 8, value: 1 }" + - "ArrayNew { dest: 4 }" + - "Load { dest: 5, literal_idx: 2 }" + - "ArrayPush { arr: 4, value: 5 }" + - "Load { dest: 6, literal_idx: 3 }" + - "ArrayPush { arr: 4, value: 6 }" + - "ArrayPush { arr: 8, value: 4 }" + - "ComprehensionBegin { params_index: 0 }" # Start outer comprehension + - "LoopStart { params_index: 0 }" # ForEach over outer array + - "Move { dest: 12, src: 11 }" # inner := outer + - "ComprehensionBegin { params_index: 1 }" # Start inner comprehension + - "LoopStart { params_index: 1 }" # ForEach over inner array + - "ComprehensionYield { value_reg: 21 }" # Yield y + - "LoopNext { body_start: 18, loop_end: 20 }" + - "ComprehensionEnd" # End inner comprehension + - "ComprehensionYield { value_reg: 14 }" # Yield inner comprehension result + - "LoopNext { body_start: 15, loop_end: 23 }" + - "ComprehensionEnd" # End outer comprehension + - "Return { value: 0 }" + want_result: [[1, 2], [3, 4]] + + - note: register_pressure_nested_loops + description: Nested loops with 15+ active registers to test allocation + example_rego: | + some a in [1]; some b in [2]; a + b > 0 + literals: + - 1 + - 2 + - 0 + instruction_params: + loop_params: + - mode: "Any" + collection: 0 + key_reg: 10 + value_reg: 11 + result_reg: 12 + body_start: 4 + loop_end: 19 + - mode: "Any" + collection: 2 + key_reg: 20 + value_reg: 21 + result_reg: 22 + body_start: 8 + loop_end: 17 + instructions: + - "ArrayNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "ArrayPush { arr: 0, value: 1 }" + - "LoopStart { params_index: 0 }" + - "ArrayNew { dest: 2 }" + - "Load { dest: 3, literal_idx: 1 }" + - "ArrayPush { arr: 2, value: 3 }" + - "LoopStart { params_index: 1 }" + # Use many registers to create pressure + - "Move { dest: 23, src: 11 }" + - "Move { dest: 24, src: 21 }" + - "Move { dest: 25, src: 23 }" + - "Move { dest: 26, src: 24 }" + - "Add { dest: 27, left: 25, right: 26 }" + - "Load { dest: 28, literal_idx: 2 }" + - "Gt { dest: 29, left: 27, right: 28 }" + - "AssertCondition { condition: 29 }" + - "LoopNext { body_start: 6, loop_end: 14 }" + - "AssertCondition { condition: 22 }" + - "LoopNext { body_start: 3, loop_end: 16 }" + - "Return { value: 12 }" + want_result: true diff --git a/tests/rvm/vm/suites/default_rules.yaml b/tests/rvm/vm/suites/default_rules.yaml new file mode 100644 index 0000000..d5b6c78 --- /dev/null +++ b/tests/rvm/vm/suites/default_rules.yaml @@ -0,0 +1,277 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Default Rules Infrastructure Test Suite +# Tests the VM infrastructure support for default rule evaluation +# These tests verify that the VM can properly handle complete rules with default fallbacks + +cases: + - note: vm_default_rule_boolean_false + description: Test VM default rule evaluation with boolean false value + example_rego: "default allow := false; allow := true if { false }" + literals: + - false + - true + rule_infos: + - rule_type: "Complete" + definitions: [[2]] # Rule definition starts at instruction 2 + default_literal_index: 0 # Points to literal index 0 (false) + instructions: + - "CallRule { dest: 0, rule_index: 0 }" # Call the complete rule + - "Return { value: 0 }" # Return result + - "RuleInit { result_reg: 1, rule_index: 0 }" # Initialize rule execution + - "Load { dest: 2, literal_idx: 0 }" # Load false condition + - "AssertCondition { condition: 2 }" # Assert condition (will fail) + - "Load { dest: 3, literal_idx: 1 }" # Load true result + - "Move { dest: 1, src: 3 }" # Move result to result register + - "RuleReturn" # Return from rule + want_result: false # Should return default value when condition fails + + - note: vm_default_rule_boolean_true + description: Test VM default rule evaluation with boolean true value + example_rego: "default enabled := true; enabled := false if { false }" + literals: + - true + - false + rule_infos: + - rule_type: "Complete" + definitions: [[2]] # Rule definition starts at instruction 2 + default_literal_index: 0 # Points to literal index 0 (true) + instructions: + - "CallRule { dest: 0, rule_index: 0 }" # Call the complete rule + - "Return { value: 0 }" # Return result + - "RuleInit { result_reg: 1, rule_index: 0 }" # Initialize rule execution + - "Load { dest: 2, literal_idx: 1 }" # Load false condition + - "AssertCondition { condition: 2 }" # Assert condition (will fail) + - "Load { dest: 3, literal_idx: 1 }" # Load false result + - "Move { dest: 1, src: 3 }" # Move result to result register + - "RuleReturn" # Return from rule + want_result: true # Should return default value when condition fails + + - note: vm_default_rule_string_value + description: Test VM default rule evaluation with string value + example_rego: "default message := \"default\"; message := \"success\" if { false }" + literals: + - "default" + - "success" + - false + rule_infos: + - rule_type: "Complete" + definitions: [[2]] # Rule definition starts at instruction 2 + default_literal_index: 0 # Points to literal index 0 ("default") + instructions: + - "CallRule { dest: 0, rule_index: 0 }" # Call the complete rule + - "Return { value: 0 }" # Return result + - "RuleInit { result_reg: 1, rule_index: 0 }" # Initialize rule execution + - "Load { dest: 2, literal_idx: 2 }" # Load false condition + - "AssertCondition { condition: 2 }" # Assert condition (will fail) + - "Load { dest: 3, literal_idx: 1 }" # Load success result + - "Move { dest: 1, src: 3 }" # Move result to result register + - "RuleReturn" # Return from rule + want_result: "default" # Should return default value when condition fails + + - note: vm_default_rule_number_value + description: Test VM default rule evaluation with numeric value + example_rego: "default count := 0; count := 10 if { false }" + literals: + - 0 + - 10 + - false + rule_infos: + - rule_type: "Complete" + definitions: [[2]] # Rule definition starts at instruction 2 + default_literal_index: 0 # Points to literal index 0 (0) + instructions: + - "CallRule { dest: 0, rule_index: 0 }" # Call the complete rule + - "Return { value: 0 }" # Return result + - "RuleInit { result_reg: 1, rule_index: 0 }" # Initialize rule execution + - "Load { dest: 2, literal_idx: 2 }" # Load false condition + - "AssertCondition { condition: 2 }" # Assert condition (will fail) + - "Load { dest: 3, literal_idx: 1 }" # Load 10 result + - "Move { dest: 1, src: 3 }" # Move result to result register + - "RuleReturn" # Return from rule + want_result: 0 # Should return default value when condition fails + + - note: vm_default_rule_array_value + description: Test VM default rule evaluation with array value + example_rego: "default items := [\"default\"]; items := [\"success\"] if { false }" + literals: + - ["default"] + - ["success"] + - false + rule_infos: + - rule_type: "Complete" + definitions: [[2]] # Rule definition starts at instruction 2 + default_literal_index: 0 # Points to literal index 0 (["default"]) + instructions: + - "CallRule { dest: 0, rule_index: 0 }" # Call the complete rule + - "Return { value: 0 }" # Return result + - "RuleInit { result_reg: 1, rule_index: 0 }" # Initialize rule execution + - "Load { dest: 2, literal_idx: 2 }" # Load false condition + - "AssertCondition { condition: 2 }" # Assert condition (will fail) + - "Load { dest: 3, literal_idx: 1 }" # Load success array + - "Move { dest: 1, src: 3 }" # Move result to result register + - "RuleReturn" # Return from rule + want_result: ["default"] # Should return default value when condition fails + + - note: vm_default_rule_object_value + description: Test VM default rule evaluation with object value + example_rego: "default config := {\"mode\": \"safe\"}; config := {\"mode\": \"fast\"} if { false }" + literals: + - {"mode": "safe"} + - {"mode": "fast"} + - false + rule_infos: + - rule_type: "Complete" + definitions: [[2]] # Rule definition starts at instruction 2 + default_literal_index: 0 # Points to literal index 0 ({"mode": "safe"}) + instructions: + - "CallRule { dest: 0, rule_index: 0 }" # Call the complete rule + - "Return { value: 0 }" # Return result + - "RuleInit { result_reg: 1, rule_index: 0 }" # Initialize rule execution + - "Load { dest: 2, literal_idx: 2 }" # Load false condition + - "AssertCondition { condition: 2 }" # Assert condition (will fail) + - "Load { dest: 3, literal_idx: 1 }" # Load fast config + - "Move { dest: 1, src: 3 }" # Move result to result register + - "RuleReturn" # Return from rule + want_result: + mode: "safe" # Should return default value when condition fails + + - note: vm_default_rule_null_value + description: Test VM default rule evaluation with null value + example_rego: "default optional := null; optional := \"value\" if { false }" + literals: + - null + - "value" + - false + rule_infos: + - rule_type: "Complete" + definitions: [[2]] # Rule definition starts at instruction 2 + default_literal_index: 0 # Points to literal index 0 (null) + instructions: + - "CallRule { dest: 0, rule_index: 0 }" # Call the complete rule + - "Return { value: 0 }" # Return result + - "RuleInit { result_reg: 1, rule_index: 0 }" # Initialize rule execution + - "Load { dest: 2, literal_idx: 2 }" # Load false condition + - "AssertCondition { condition: 2 }" # Assert condition (will fail) + - "Load { dest: 3, literal_idx: 1 }" # Load string value + - "Move { dest: 1, src: 3 }" # Move result to result register + - "RuleReturn" # Return from rule + want_result: null # Should return default value when condition fails + + - note: vm_default_rule_multiple_definitions_fail + description: Test VM default rule when multiple definitions all fail + example_rego: "default result := \"fallback\"; result := \"first\" if { false }; result := \"second\" if { false }" + literals: + - "fallback" + - "first" + - "second" + - false + rule_infos: + - rule_type: "Complete" + definitions: [[2], [7]] # Two rule definitions + default_literal_index: 0 # Points to literal index 0 ("fallback") + instructions: + - "CallRule { dest: 0, rule_index: 0 }" # Call the complete rule + - "Return { value: 0 }" # Return result + # First definition + - "RuleInit { result_reg: 1, rule_index: 0 }" # Initialize rule execution + - "Load { dest: 2, literal_idx: 3 }" # Load false condition + - "AssertCondition { condition: 2 }" # Assert condition (will fail) + - "Load { dest: 3, literal_idx: 1 }" # Load first result + - "Move { dest: 1, src: 3 }" # Move result to result register + - "RuleReturn" # Return from rule + # Second definition + - "RuleInit { result_reg: 1, rule_index: 0 }" # Initialize rule execution + - "Load { dest: 4, literal_idx: 3 }" # Load false condition + - "AssertCondition { condition: 4 }" # Assert condition (will fail) + - "Load { dest: 5, literal_idx: 2 }" # Load second result + - "Move { dest: 1, src: 5 }" # Move result to result register + - "RuleReturn" # Return from rule + want_result: "fallback" # Should return default value when all definitions fail + + - note: vm_default_rule_successful_condition + description: Test VM rule evaluation when condition succeeds (should not use default) + example_rego: "default allow := false; allow := true if { true }" + literals: + - false + - true + rule_infos: + - rule_type: "Complete" + definitions: [[2]] # Rule definition starts at instruction 2 + default_literal_index: 0 # Points to literal index 0 (false) - should not be used + instructions: + - "CallRule { dest: 0, rule_index: 0 }" # Call the complete rule + - "Return { value: 0 }" # Return result + - "RuleInit { result_reg: 1, rule_index: 0 }" # Initialize rule execution + - "Load { dest: 2, literal_idx: 1 }" # Load true condition + - "AssertCondition { condition: 2 }" # Assert condition (will succeed) + - "Load { dest: 3, literal_idx: 1 }" # Load true result + - "Move { dest: 1, src: 3 }" # Move result to result register + - "RuleReturn" # Return from rule + want_result: true # Should return successful rule result, not default + + - note: vm_default_rule_complex_object + description: Test VM default rule with complex nested object + example_rego: "default settings := complex_default; settings := complex_success if { false }" + literals: + - { + "timeout": 30, + "retries": 3, + "features": { + "logging": true, + "monitoring": false + }, + "endpoints": ["api1", "api2"] + } + - { + "timeout": 60, + "retries": 5, + "features": { + "logging": false, + "monitoring": true + }, + "endpoints": ["api3", "api4"] + } + - false + rule_infos: + - rule_type: "Complete" + definitions: [[2]] # Rule definition starts at instruction 2 + default_literal_index: 0 # Points to literal index 0 (complex default object) + instructions: + - "CallRule { dest: 0, rule_index: 0 }" # Call the complete rule + - "Return { value: 0 }" # Return result + - "RuleInit { result_reg: 1, rule_index: 0 }" # Initialize rule execution + - "Load { dest: 2, literal_idx: 2 }" # Load false condition + - "AssertCondition { condition: 2 }" # Assert condition (will fail) + - "Load { dest: 3, literal_idx: 1 }" # Load success object + - "Move { dest: 1, src: 3 }" # Move result to result register + - "RuleReturn" # Return from rule + want_result: + timeout: 30 + retries: 3 + features: + logging: true + monitoring: false + endpoints: ["api1", "api2"] # Should return default complex object + + - note: vm_no_default_rule_undefined + description: Test VM rule without default returns undefined when definition fails + example_rego: "allow := true if { false } # No default rule" + literals: + - true + - false + rule_infos: + - rule_type: "Complete" + definitions: [[2]] # Rule definition starts at instruction 2 + # No default_literal_index - should return undefined + instructions: + - "CallRule { dest: 0, rule_index: 0 }" # Call the complete rule + - "Return { value: 0 }" # Return result + - "RuleInit { result_reg: 1, rule_index: 0 }" # Initialize rule execution + - "Load { dest: 2, literal_idx: 1 }" # Load false condition + - "AssertCondition { condition: 2 }" # Assert condition (will fail) + - "Load { dest: 3, literal_idx: 0 }" # Load true result + - "Move { dest: 1, src: 3 }" # Move result to result register + - "RuleReturn" # Return from rule + want_result: "#undefined" # Should return undefined when no default and definition fails diff --git a/tests/rvm/vm/suites/destructuring_rules.yaml b/tests/rvm/vm/suites/destructuring_rules.yaml new file mode 100644 index 0000000..c202544 --- /dev/null +++ b/tests/rvm/vm/suites/destructuring_rules.yaml @@ -0,0 +1,41 @@ +# Destructuring Rules Test Suite +# Covers DestructuringSuccess control flow for rule parameters. + +cases: + - note: destructuring_success_path + description: Destructuring block passes and body executes + literals: + - 1 + rule_infos: + - rule_type: Complete + definitions: + - [4] + destructuring_blocks: [2] + instructions: + - "CallRule { dest: 0, rule_index: 0 }" + - "Return { value: 0 }" + - "Load { dest: 2, literal_idx: 0 }" + - "DestructuringSuccess {}" + - "RuleInit { result_reg: 1, rule_index: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "RuleReturn {}" + want_result: 1 + + - note: destructuring_failure_skips_body + description: Missing DestructuringSuccess causes undefined result + literals: + - 1 + rule_infos: + - rule_type: Complete + definitions: + - [4] + destructuring_blocks: [2] + instructions: + - "CallRule { dest: 0, rule_index: 0 }" + - "Return { value: 0 }" + - "LoadFalse { dest: 2 }" + - "AssertCondition { condition: 2 }" + - "RuleInit { result_reg: 1, rule_index: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "RuleReturn {}" + want_result: "#undefined" diff --git a/tests/rvm/vm/suites/function_calls.yaml b/tests/rvm/vm/suites/function_calls.yaml new file mode 100644 index 0000000..fc23a15 --- /dev/null +++ b/tests/rvm/vm/suites/function_calls.yaml @@ -0,0 +1,371 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Function Call Instructions Test Suite +# Tests the VM's function call mechanism with user-defined function rules +# Covers FunctionCall instruction, argument passing, and return values + +cases: + # Basic Function Call Tests + - note: simple_function_call + description: Test basic function call instruction with one argument + example_rego: "add_ten(5) where add_ten(x) := x + 10" + literals: + - {} + - 5 # Argument value + - 10 # Constant 10 for addition + instruction_params: + object_create_params: + - dest: 6 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + function_call_params: + - func: 0 # Rule index for add_ten function (rule 0) + dest: 2 # Destination register for result + args: [1] # Argument registers + rule_infos: + - rule_type: Complete + definitions: + - [3] # Entry point for add_ten function body (PC 3) + instructions: + - "Load { dest: 1, literal_idx: 1 }" # Load argument 5 into register 1 + - "FunctionCall { params_index: 0 }" # Call function + - "Return { value: 2 }" # Return result + # Function body starts at PC 3 (entry point) + - "RuleInit { result_reg: 0, rule_index: 0 }" # Initialize result register + - "Load { dest: 3, literal_idx: 2 }" # Load constant 10 into register 3 + - "Add { dest: 0, left: 1, right: 3 }" # Add argument + 10 (result in register 0) + - "RuleReturn {}" # Return from function + want_result: 15 + + - note: function_call_multiple_args + description: Test function call with multiple arguments + example_rego: "add(7, 3) where add(x, y) := x + y" + literals: + - {} + - 0 # Rule index for add function + - 7 # First argument + - 3 # Second argument + instruction_params: + function_call_params: + - func: 0 # Register containing function rule index + dest: 3 # Destination register for result + args: [1, 2] # Argument registers + rule_infos: + - rule_type: Complete + definitions: + - [5] # Entry point for add function body + instructions: + - "Load { dest: 0, literal_idx: 1 }" # Load rule index + - "Load { dest: 1, literal_idx: 2 }" # Load first argument + - "Load { dest: 2, literal_idx: 3 }" # Load second argument + - "FunctionCall { params_index: 0 }" # Call function + - "Return { value: 3 }" # Return result + # Function body + - "RuleInit { result_reg: 0, rule_index: 0 }" # Initialize result register + - "Add { dest: 0, left: 1, right: 2 }" # Add arguments (result in register 0) + - "RuleReturn {}" # Return from function + want_result: 10 + + - note: function_call_no_args + description: Test function call with no arguments + example_rego: "get_constant() where get_constant() := 42" + literals: + - {} + - 0 # Rule index for get_constant function + - 42 # Constant value + instruction_params: + function_call_params: + - func: 0 # Register containing function rule index + dest: 1 # Destination register for result + args: [] # No arguments + rule_infos: + - rule_type: Complete + definitions: + - [3] # Entry point for get_constant function body + instructions: + - "Load { dest: 0, literal_idx: 1 }" # Load rule index + - "FunctionCall { params_index: 0 }" # Call function + - "Return { value: 1 }" # Return result + # Function body + - "RuleInit { result_reg: 0, rule_index: 0 }" # Initialize result register + - "Load { dest: 0, literal_idx: 2 }" # Load constant 42 (result in register 0) + - "RuleReturn {}" # Return from function + want_result: 42 + + - note: function_call_with_multiplication + description: Test function that performs multiplication + example_rego: "square(6) where square(x) := x * x" + literals: + - {} + - 0 # Rule index for square function + - 6 # Argument value + instruction_params: + function_call_params: + - func: 0 # Register containing function rule index + dest: 2 # Destination register for result + args: [1] # Argument registers + rule_infos: + - rule_type: Complete + definitions: + - [4] # Entry point for square function body + instructions: + - "Load { dest: 0, literal_idx: 1 }" # Load rule index + - "Load { dest: 1, literal_idx: 2 }" # Load argument 6 + - "FunctionCall { params_index: 0 }" # Call function + - "Return { value: 2 }" # Return result + # Function body + - "RuleInit { result_reg: 0, rule_index: 0 }" # Initialize result register + - "Mul { dest: 0, left: 1, right: 1 }" # Multiply x * x (result in register 0) + - "RuleReturn {}" # Return from function + want_result: 36 + + - note: nested_function_calls + description: Test nested function calls + example_rego: "double(add_one(5)) where double(x) := x * 2; add_one(x) := x + 1" + literals: + - {} + - 1 # Rule index for add_one function + - 0 # Rule index for double function + - 5 # Initial argument + - 1 # Constant 1 + - 2 # Constant 2 + instruction_params: + function_call_params: + - func: 1 # First call: add_one (rule index 1) + dest: 4 # Temporary result + args: [2] # Argument register + - func: 0 # Second call: double (rule index 0) + dest: 5 # Final result + args: [4] # Use result from first call + rule_infos: + - rule_type: Complete # double function + definitions: + - [10] # Entry point for double function body (updated) + - rule_type: Complete # add_one function + definitions: + - [6] # Entry point for add_one function body + instructions: + - "Load { dest: 0, literal_idx: 1 }" # Load add_one rule index + - "Load { dest: 1, literal_idx: 2 }" # Load double rule index + - "Load { dest: 2, literal_idx: 3 }" # Load argument 5 + - "FunctionCall { params_index: 0 }" # Call add_one(5) + - "FunctionCall { params_index: 1 }" # Call double(result) + - "Return { value: 5 }" # Return final result + # add_one function body (starts at PC 6) + - "RuleInit { result_reg: 0, rule_index: 1 }" # Initialize result register for add_one + - "Load { dest: 3, literal_idx: 4 }" # Load constant 1 + - "Add { dest: 0, left: 1, right: 3 }" # x + 1 (result in register 0) + - "RuleReturn {}" # Return result + # double function body (starts at PC 10) + - "RuleInit { result_reg: 0, rule_index: 0 }" # Initialize result register for double + - "Load { dest: 3, literal_idx: 5 }" # Load constant 2 + - "Mul { dest: 0, left: 1, right: 3 }" # x * 2 (result in register 0) + - "RuleReturn {}" # Return result + want_result: 12 + + - note: function_call_undefined_result + description: Test function call that returns undefined due to failed condition + example_rego: "safe_div(1, 0) where safe_div(x, y) := x / y if y != 0" + literals: + - {} + - 0 # Rule index for safe_div function + - 1 # Numerator + - 0 # Denominator (zero) + instruction_params: + function_call_params: + - func: 0 # Register containing function rule index + dest: 3 # Destination register for result + args: [1, 2] # Argument registers + rule_infos: + - rule_type: Complete + definitions: + - [5] # Entry point for safe_div function body + instructions: + - "Load { dest: 0, literal_idx: 1 }" # Load rule index + - "Load { dest: 1, literal_idx: 2 }" # Load numerator + - "Load { dest: 2, literal_idx: 3 }" # Load denominator + - "FunctionCall { params_index: 0 }" # Call function + - "Return { value: 3 }" # Return result + # Function body with condition that fails + - "RuleInit { result_reg: 0, rule_index: 0 }" # Initialize result register + - "LoadFalse { dest: 4 }" # Condition fails (simulated y == 0) + - "AssertCondition { condition: 4 }" # Assert fails, body fails + - "Div { dest: 0, left: 1, right: 2 }" # This won't execute + - "RuleReturn {}" # This won't execute + want_result: "#undefined" + + - note: function_call_with_object_result + description: Test function call that returns an object + example_rego: "make_pair(1, 2) where make_pair(x, y) := {\"first\": x, \"second\": y}" + literals: + - {} + - 0 # Rule index for make_pair function + - 1 # First value + - 2 # Second value + - "first" # Key for first element + - "second" # Key for second element + instruction_params: + object_create_params: + - dest: 6 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + function_call_params: + - func: 0 # Register containing function rule index + dest: 5 # Destination register for result + args: [1, 2] # Argument registers + rule_infos: + - rule_type: Complete + definitions: + - [5] # Entry point for make_pair function body + instructions: + - "Load { dest: 0, literal_idx: 1 }" # Load rule index + - "Load { dest: 1, literal_idx: 2 }" # Load first value + - "Load { dest: 2, literal_idx: 3 }" # Load second value + - "FunctionCall { params_index: 0 }" # Call function + - "Return { value: 5 }" # Return result + # Function body + - "RuleInit { result_reg: 0, rule_index: 0 }" # Initialize result register + - "ObjectCreate { params_index: 0 }" + - "Load { dest: 3, literal_idx: 4 }" # Load "first" key + - "ObjectSet { obj: 6, key: 3, value: 1 }" # Set first: x + - "Load { dest: 4, literal_idx: 5 }" # Load "second" key + - "ObjectSet { obj: 6, key: 4, value: 2 }" # Set second: y + - "Move { dest: 0, src: 6 }" # Move object to result register + - "RuleReturn {}" # Return object + want_result: {"first": 1, "second": 2} + + - note: function_call_with_array_result + description: Test function call that returns an array + example_rego: "make_range(3, 5) where make_range(start, end) := [start, end]" + literals: + - {} + - 0 # Rule index for make_range function + - 3 # Start value + - 5 # End value + instruction_params: + function_call_params: + - func: 0 # Register containing function rule index + dest: 3 # Destination register for result + args: [1, 2] # Argument registers + rule_infos: + - rule_type: Complete + definitions: + - [5] # Entry point for make_range function body + instructions: + - "Load { dest: 0, literal_idx: 1 }" # Load rule index + - "Load { dest: 1, literal_idx: 2 }" # Load start value + - "Load { dest: 2, literal_idx: 3 }" # Load end value + - "FunctionCall { params_index: 0 }" # Call function + - "Return { value: 3 }" # Return result + # Function body + - "RuleInit { result_reg: 0, rule_index: 0 }" # Initialize result register + - "ArrayNew { dest: 4 }" # Create new array + - "ArrayPush { arr: 4, value: 1 }" # Push start value + - "ArrayPush { arr: 4, value: 2 }" # Push end value + - "Move { dest: 0, src: 4 }" # Move array to result register + - "RuleReturn {}" # Return array + want_result: [3, 5] + + - note: function_call_with_comparison + description: Test function that performs comparison + example_rego: "max(7, 3) where max(x, y) := x if x >= y" + literals: + - {} + - 0 # Rule index for max function + - 7 # First value + - 3 # Second value + instruction_params: + function_call_params: + - func: 0 # Register containing function rule index + dest: 3 # Destination register for result + args: [1, 2] # Argument registers + rule_infos: + - rule_type: Complete + definitions: + - [5] # Entry point for max function body + instructions: + - "Load { dest: 0, literal_idx: 1 }" # Load rule index + - "Load { dest: 1, literal_idx: 2 }" # Load first value (7) + - "Load { dest: 2, literal_idx: 3 }" # Load second value (3) + - "FunctionCall { params_index: 0 }" # Call function + - "Return { value: 3 }" # Return result + # Function body: return x if x >= y + - "RuleInit { result_reg: 0, rule_index: 0 }" # Initialize result register + - "Ge { dest: 4, left: 1, right: 2 }" # x >= y + - "AssertCondition { condition: 4 }" # Assert condition + - "Move { dest: 0, src: 1 }" # Return x (move to result register) + - "RuleReturn {}" # Return result + want_result: 7 + + - note: function_call_three_args + description: Test function call with three arguments + example_rego: "sum_three(2, 3, 4) where sum_three(a, b, c) := a + b + c" + literals: + - {} + - 0 # Rule index for sum_three function + - 2 # First argument + - 3 # Second argument + - 4 # Third argument + instruction_params: + function_call_params: + - func: 0 # Register containing function rule index + dest: 4 # Destination register for result + args: [1, 2, 3] # Argument registers + rule_infos: + - rule_type: Complete + definitions: + - [6] # Entry point for sum_three function body + instructions: + - "Load { dest: 0, literal_idx: 1 }" # Load rule index + - "Load { dest: 1, literal_idx: 2 }" # Load first argument (2) + - "Load { dest: 2, literal_idx: 3 }" # Load second argument (3) + - "Load { dest: 3, literal_idx: 4 }" # Load third argument (4) + - "FunctionCall { params_index: 0 }" # Call function + - "Return { value: 4 }" # Return result + # Function body: a + b + c + - "RuleInit { result_reg: 0, rule_index: 0 }" # Initialize result register + - "Add { dest: 5, left: 1, right: 2 }" # a + b + - "Add { dest: 0, left: 5, right: 3 }" # (a + b) + c (result in register 0) + - "RuleReturn {}" # Return result + want_result: 9 + + - note: function_call_inconsistent_definitions + description: Test function with multiple definitions that produce different values (should fail) + example_rego: "f(5) where f(x) := x + 1; f(x) := x + 2" + literals: + - {} + - 0 # Rule index for f function + - 5 # Argument value + - 1 # Constant 1 + - 2 # Constant 2 + instruction_params: + function_call_params: + - func: 0 # Register containing function rule index + dest: 2 # Destination register for result + args: [1] # Argument registers + rule_infos: + - rule_type: Complete + definitions: + - [4] # First definition: x + 1 + - [8] # Second definition: x + 2 (should produce different result) + entry_points: + "data.test.compute": [4, 8] # Multiple definitions for the same function + instructions: + - "Load { dest: 0, literal_idx: 1 }" # Load rule index + - "Load { dest: 1, literal_idx: 2 }" # Load argument (5) + - "FunctionCall { params_index: 0 }" # Call function + - "Return { value: 2 }" # Return result + # First definition: x + 1 (entry point 4) + - "RuleInit { result_reg: 0, rule_index: 0 }" # Initialize result register + - "Load { dest: 3, literal_idx: 3 }" # Load constant 1 + - "Add { dest: 0, left: 1, right: 3 }" # x + 1 = 6 (result in register 0) + - "RuleReturn {}" # Return result + # Second definition: x + 2 (entry point 8) + - "RuleInit { result_reg: 0, rule_index: 0 }" # Initialize result register + - "Load { dest: 4, literal_idx: 4 }" # Load constant 2 + - "Add { dest: 0, left: 1, right: 4 }" # x + 2 = 7 (result in register 0) + - "RuleReturn {}" # Return result + want_result: "#undefined" diff --git a/tests/rvm/vm/suites/halt.yaml b/tests/rvm/vm/suites/halt.yaml new file mode 100644 index 0000000..d0d28b2 --- /dev/null +++ b/tests/rvm/vm/suites/halt.yaml @@ -0,0 +1,14 @@ +# Halt Instruction Test Suite +# Ensures Halt returns register 0 and ignores trailing instructions. + +cases: + - note: halt_returns_register_zero + description: Halt ends execution and returns register 0 + literals: + - 42 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Halt" + - "Load { dest: 1, literal_idx: 0 }" + - "Return { value: 1 }" + want_result: 42 diff --git a/tests/rvm/vm/suites/host_await.yaml b/tests/rvm/vm/suites/host_await.yaml new file mode 100644 index 0000000..7d8bf65 --- /dev/null +++ b/tests/rvm/vm/suites/host_await.yaml @@ -0,0 +1,137 @@ +# HostAwait integration test suite +# Verifies host suspension across execution modes and optional run-to-completion fallbacks. + +cases: + - note: host_await_single_response + description: Single HostAwait resumes with provided response in every execution mode + literals: + - "ping" + - "await-0" + instructions: + - "ArrayNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 3, literal_idx: 1 }" + - "HostAwait { dest: 2, arg: 1, id: 3 }" + - "ArrayPush { arr: 0, value: 2 }" + - "Return { value: 0 }" + host_await_responses: + - id: "await-0" + value: "pong" + want_result: + - "pong" + + - note: host_await_multiple_responses + description: Multiple HostAwait instructions consume a queued sequence of responses + literals: + - 7 + - "await-first" + - "await-second" + instructions: + - "ArrayNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 4, literal_idx: 1 }" + - "HostAwait { dest: 2, arg: 1, id: 4 }" + - "Load { dest: 5, literal_idx: 2 }" + - "HostAwait { dest: 3, arg: 2, id: 5 }" + - "ArrayPush { arr: 0, value: 2 }" + - "ArrayPush { arr: 0, value: 3 }" + - "Return { value: 0 }" + host_await_responses_run_to_completion: + - id: "await-first" + value: 42 + - id: "await-second" + value: 43 + host_await_responses_suspendable: + - id: "await-first" + value: 42 + - id: "await-second" + value: 43 + want_result: + - 42 + - 43 + + - note: host_await_suspendable_only + description: Run-to-completion failure due to missing HostAwait response is ignored when flagged + literals: + - "payload" + - "await-single" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "HostAwait { dest: 1, arg: 0, id: 2 }" + - "Return { value: 1 }" + host_await_responses_suspendable: + - id: "await-single" + value: "resume-value" + ignore_run_to_completion_hostawait_failure: true + want_result: "resume-value" + + - note: host_await_multiple_sequential + description: Three sequential HostAwait calls with different responses + literals: + - "first" + - "second" + - "third" + - "id-1" + - "id-2" + - "id-3" + instructions: + - "ArrayNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 10, literal_idx: 3 }" + - "HostAwait { dest: 2, arg: 1, id: 10 }" + - "ArrayPush { arr: 0, value: 2 }" + - "Load { dest: 3, literal_idx: 1 }" + - "Load { dest: 11, literal_idx: 4 }" + - "HostAwait { dest: 4, arg: 3, id: 11 }" + - "ArrayPush { arr: 0, value: 4 }" + - "Load { dest: 5, literal_idx: 2 }" + - "Load { dest: 12, literal_idx: 5 }" + - "HostAwait { dest: 6, arg: 5, id: 12 }" + - "ArrayPush { arr: 0, value: 6 }" + - "Return { value: 0 }" + host_await_responses: + - id: "id-1" + value: "response-1" + - id: "id-2" + value: "response-2" + - id: "id-3" + value: "response-3" + want_result: ["response-1", "response-2", "response-3"] + + - note: host_await_in_loop_body + description: HostAwait inside loop body - suspend/resume per iteration + literals: + - 1 + - 2 + - "id-1" + - "id-2" + instruction_params: + loop_params: + - mode: "ForEach" + collection: 5 + key_reg: 10 + value_reg: 11 + result_reg: 12 + body_start: 7 + loop_end: 11 + instructions: + - "ArrayNew { dest: 5 }" # Collection [1, 2] + - "Load { dest: 1, literal_idx: 0 }" + - "ArrayPush { arr: 5, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "ArrayPush { arr: 5, value: 2 }" + - "ArrayNew { dest: 0 }" # Result array + - "LoopStart { params_index: 0 }" + # HostAwait with dynamic ID based on loop value + - "Load { dest: 13, literal_idx: 2 }" # Load base ID + - "HostAwait { dest: 14, arg: 11, id: 13 }" + - "ArrayPush { arr: 0, value: 14 }" + - "LoopNext { body_start: 7, loop_end: 11 }" + - "Return { value: 0 }" + host_await_responses: + - id: "id-1" + value: "loop-response-1" + - id: "id-1" + value: "loop-response-2" + want_result: ["loop-response-1", "loop-response-2"] diff --git a/tests/rvm/vm/suites/host_await_failures.yaml b/tests/rvm/vm/suites/host_await_failures.yaml new file mode 100644 index 0000000..86421fe --- /dev/null +++ b/tests/rvm/vm/suites/host_await_failures.yaml @@ -0,0 +1,28 @@ +# Host Await Failures Test Suite +# Validates HostAwait error surfaces under different execution modes. + +cases: + - note: host_await_missing_response + description: Run-to-completion mode errors when responses absent + literals: + - "request" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "HostAwait { dest: 1, arg: 0, id: 0 }" + - "Return { value: 1 }" + want_error: "HostAwait executed but no response provided" + + - note: host_await_ignore_flag + description: ignore_run_to_completion_hostawait_failure guards missing responses + literals: + - "request" + - "response" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "HostAwait { dest: 1, arg: 0, id: 0 }" + - "Return { value: 1 }" + host_await_responses_suspendable: + - id: "request" + value: "response" + ignore_run_to_completion_hostawait_failure: true + want_result: "response" diff --git a/tests/rvm/vm/suites/indexed_access.yaml b/tests/rvm/vm/suites/indexed_access.yaml new file mode 100644 index 0000000..7bbf40e --- /dev/null +++ b/tests/rvm/vm/suites/indexed_access.yaml @@ -0,0 +1,241 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Indexed Access Test Suite +# Exercises literal indexing, chained indexing, and error paths. + +cases: + - note: index_literal_success + description: IndexLiteral retrieves value when literal key exists + literals: + - { key: true } + - "key" + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + instructions: + - "ObjectCreate { params_index: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "LoadTrue { dest: 2 }" + - "ObjectSet { obj: 0, key: 1, value: 2 }" + - "IndexLiteral { dest: 3, container: 0, literal_idx: 1 }" + - "Return { value: 3 }" + want_result: true + + - note: index_literal_bad_literal + description: IndexLiteral with invalid literal index triggers error + literals: + - {} + instructions: + - "ObjectCreate { params_index: 0 }" + - "IndexLiteral { dest: 1, container: 0, literal_idx: 99 }" + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + want_error: "Literal index 99 out of bounds" + + - note: chained_index_mixed_path + description: ChainedIndex handles literal and register path components + literals: + - {} + - "outer" + - "inner" + - "value" + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + chained_index_params: + - dest: 5 + root: 0 + path_components: + - literal_idx: 1 + - register: 3 + instructions: + - "ObjectCreate { params_index: 0 }" + - "Load { dest: 3, literal_idx: 2 }" # inner + - "Load { dest: 4, literal_idx: 3 }" # value + - "Load { dest: 2, literal_idx: 0 }" # nested object builder + - "ObjectSet { obj: 2, key: 3, value: 4 }" + - "Load { dest: 1, literal_idx: 1 }" # outer + - "ObjectSet { obj: 0, key: 1, value: 2 }" + - "ChainedIndex { params_index: 0 }" + - "Return { value: 5 }" + want_result: "value" + + - note: chained_index_undefined_propagation + description: ChainedIndex yields undefined when intermediate missing + literals: + - {} + - "missing" + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + chained_index_params: + - dest: 2 + root: 0 + path_components: + - literal_idx: 1 + - literal_idx: 1 + instructions: + - "ObjectCreate { params_index: 0 }" + - "ChainedIndex { params_index: 0 }" + - "Return { value: 2 }" + want_result: "#undefined" + + - note: chained_index_5_components + description: ChainedIndex with 5 path components + example_rego: "data.a.b.c.d.e" + literals: + - {} + - "a" + - "b" + - "c" + - "d" + - "e" + - "final_value" + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + chained_index_params: + - dest: 10 + root: 0 + path_components: + - literal_idx: 1 # a + - literal_idx: 2 # b + - literal_idx: 3 # c + - literal_idx: 4 # d + - literal_idx: 5 # e + instructions: + - "ObjectCreate { params_index: 0 }" # r0 = {} + # Build nested structure: {a: {b: {c: {d: {e: "final_value"}}}}} + - "Load { dest: 1, literal_idx: 0 }" # innermost object + - "Load { dest: 2, literal_idx: 5 }" # key "e" + - "Load { dest: 3, literal_idx: 6 }" # value "final_value" + - "ObjectSet { obj: 1, key: 2, value: 3 }" # {e: "final_value"} + - "Load { dest: 4, literal_idx: 0 }" # d level object + - "Load { dest: 5, literal_idx: 4 }" # key "d" + - "ObjectSet { obj: 4, key: 5, value: 1 }" # {d: {e: ...}} + - "Load { dest: 6, literal_idx: 0 }" # c level object + - "Load { dest: 7, literal_idx: 3 }" # key "c" + - "ObjectSet { obj: 6, key: 7, value: 4 }" # {c: {d: ...}} + - "Load { dest: 8, literal_idx: 0 }" # b level object + - "Load { dest: 9, literal_idx: 2 }" # key "b" + - "ObjectSet { obj: 8, key: 9, value: 6 }" # {b: {c: ...}} + - "Load { dest: 11, literal_idx: 1 }" # key "a" + - "ObjectSet { obj: 0, key: 11, value: 8 }" # {a: {b: ...}} + - "ChainedIndex { params_index: 0 }" + - "Return { value: 10 }" + want_result: "final_value" + + - note: chained_index_all_registers + description: ChainedIndex with all register components (no literals) + example_rego: "obj[key1][key2]" + literals: + - {} + - "level1" + - "level2" + - "result" + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + chained_index_params: + - dest: 10 + root: 0 + path_components: + - register: 5 # key1 + - register: 6 # key2 + instructions: + - "ObjectCreate { params_index: 0 }" # r0 = {} + # Build structure: {level1: {level2: "result"}} + - "Load { dest: 1, literal_idx: 0 }" # inner object + - "Load { dest: 2, literal_idx: 2 }" # "level2" + - "Load { dest: 3, literal_idx: 3 }" # "result" + - "ObjectSet { obj: 1, key: 2, value: 3 }" # {level2: "result"} + - "Load { dest: 4, literal_idx: 1 }" # "level1" + - "ObjectSet { obj: 0, key: 4, value: 1 }" # {level1: {...}} + # Set up dynamic keys in registers + - "Load { dest: 5, literal_idx: 1 }" # key1 = "level1" + - "Load { dest: 6, literal_idx: 2 }" # key2 = "level2" + - "ChainedIndex { params_index: 0 }" + - "Return { value: 10 }" + want_result: "result" + + - note: index_with_null_key + description: Index with null key returns undefined + example_rego: "obj[null]" + literals: + - {"key": "value"} + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "LoadNull { dest: 1 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" + + - note: index_with_undefined_key + description: Index with undefined key returns undefined + example_rego: "obj[undefined_var]" + literals: + - {"key": "value"} + instructions: + - "Load { dest: 0, literal_idx: 0 }" + # r1 is undefined (never loaded) + - "Index { dest: 2, container: 0, key: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" + + - note: index_with_boolean_key + description: Index with boolean key + example_rego: "obj[true]" + literals: + - {true: "bool_value", "regular": "string_value"} + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "LoadTrue { dest: 1 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Return { value: 2 }" + want_result: "bool_value" + + - note: index_with_empty_string_key + description: Index with empty string key + example_rego: "obj[\"\"]" + literals: + - {"": "empty_key_value", "other": "other_value"} + - "" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Return { value: 2 }" + want_result: "empty_key_value" + + - note: index_with_number_key_on_object + description: Index object with number key + example_rego: "obj[42]" + literals: + - {42: "number_value", "str": "string_value"} + - 42 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Return { value: 2 }" + want_result: "number_value" diff --git a/tests/rvm/vm/suites/integration_scenarios.yaml b/tests/rvm/vm/suites/integration_scenarios.yaml new file mode 100644 index 0000000..6b1c1a7 --- /dev/null +++ b/tests/rvm/vm/suites/integration_scenarios.yaml @@ -0,0 +1,432 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Integration Scenarios Test Suite +# Tests real-world Rego policy patterns +# Covers RBAC, filtering, transforms, and complex policy workflows + +cases: + - note: rbac_admin_check + description: Role-based access control - check admin role + example_rego: "allow { user.roles[_] == \"admin\" }" + literals: + - {"roles": ["user", "admin", "developer"]} + - "roles" + - "admin" + instruction_params: + loop_params: + - mode: "Any" + collection: 2 + key_reg: 3 + value_reg: 4 + result_reg: 6 + body_start: 5 + loop_end: 8 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Load { dest: 5, literal_idx: 2 }" + - "LoopStart { params_index: 0 }" + - "Eq { dest: 7, left: 4, right: 5 }" + - "AssertCondition { condition: 7 }" + - "LoopNext { body_start: 5, loop_end: 8 }" + - "Return { value: 6 }" + want_result: true + + - note: rbac_no_matching_role + description: RBAC check fails when role not present + example_rego: "allow { user.roles[_] == \"superadmin\" }" + literals: + - {"roles": ["user", "developer"]} + - "roles" + - "superadmin" + instruction_params: + loop_params: + - mode: "Any" + collection: 2 + key_reg: 3 + value_reg: 4 + result_reg: 6 + body_start: 5 + loop_end: 8 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Load { dest: 5, literal_idx: 2 }" + - "LoopStart { params_index: 0 }" + - "Eq { dest: 7, left: 4, right: 5 }" + - "AssertCondition { condition: 7 }" + - "LoopNext { body_start: 5, loop_end: 8 }" + - "Return { value: 6 }" + want_result: false + + - note: filtering_by_owner + description: Filter resources by owner ID + example_rego: "[x | x = resources[_]; x.owner == user.id]" + literals: + - [{"name": "res1", "owner": "user123"}, {"name": "res2", "owner": "user456"}, {"name": "res3", "owner": "user123"}] + - {"id": "user123"} + - "owner" + - "id" + instruction_params: + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 1 + value_reg: 2 + result_reg: 11 + body_start: 4 + loop_end: 12 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "ArrayNew { dest: 8 }" + - "Load { dest: 9, literal_idx: 1 }" + - "LoopStart { params_index: 0 }" + - "Load { dest: 3, literal_idx: 2 }" + - "Index { dest: 4, container: 2, key: 3 }" + - "Load { dest: 5, literal_idx: 3 }" + - "Index { dest: 6, container: 9, key: 5 }" + - "Eq { dest: 7, left: 4, right: 6 }" + - "AssertCondition { condition: 7 }" + - "ArrayPush { arr: 8, value: 2 }" + - "LoopNext { body_start: 4, loop_end: 12 }" + - "Return { value: 8 }" + want_result: [{"name": "res1", "owner": "user123"}, {"name": "res3", "owner": "user123"}] + + - note: filtering_empty_result + description: Filter returns empty array when no matches + example_rego: "[x | x = resources[_]; x.owner == \"nonexistent\"]" + literals: + - [{"name": "res1", "owner": "user123"}, {"name": "res2", "owner": "user456"}] + - "owner" + - "nonexistent" + instruction_params: + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 1 + value_reg: 2 + result_reg: 8 + body_start: 4 + loop_end: 10 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "ArrayNew { dest: 7 }" + - "Load { dest: 5, literal_idx: 2 }" + - "LoopStart { params_index: 0 }" + - "Load { dest: 3, literal_idx: 1 }" + - "Index { dest: 4, container: 2, key: 3 }" + - "Eq { dest: 6, left: 4, right: 5 }" + - "AssertCondition { condition: 6 }" + - "ArrayPush { arr: 7, value: 2 }" + - "LoopNext { body_start: 4, loop_end: 10 }" + - "Return { value: 7 }" + want_result: [] + + - note: transform_multiply_values + description: Transform values by multiplying above threshold + example_rego: "{k: v * 2 | v = data.metrics[k]; v > threshold}" + literals: + - {"cpu": 50, "memory": 80, "disk": 30} + - 40 + - 2 + - {} + instruction_params: + object_create_params: + - dest: 4 + template_literal_idx: 3 + literal_key_fields: [] + fields: [] + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 1 + value_reg: 2 + result_reg: 9 + body_start: 4 + loop_end: 10 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 3, literal_idx: 1 }" + - "ObjectCreate { params_index: 0 }" + - "LoopStart { params_index: 0 }" + - "Gt { dest: 5, left: 2, right: 3 }" + - "AssertCondition { condition: 5 }" + - "Load { dest: 7, literal_idx: 2 }" + - "Mul { dest: 8, left: 2, right: 7 }" + - "ObjectSet { obj: 4, key: 1, value: 8 }" + - "LoopNext { body_start: 4, loop_end: 10 }" + - "Return { value: 4 }" + want_result: {"cpu": 100, "memory": 160} + + - note: transform_all_values + description: Transform all values in map + example_rego: "{k: v + 10 | v = data[k]}" + literals: + - {"a": 1, "b": 2, "c": 3} + - 10 + - {} + instruction_params: + object_create_params: + - dest: 4 + template_literal_idx: 2 + literal_key_fields: [] + fields: [] + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 1 + value_reg: 2 + result_reg: 6 + body_start: 4 + loop_end: 7 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 3, literal_idx: 1 }" + - "ObjectCreate { params_index: 0 }" + - "LoopStart { params_index: 0 }" + - "Add { dest: 5, left: 2, right: 3 }" + - "ObjectSet { obj: 4, key: 1, value: 5 }" + - "LoopNext { body_start: 4, loop_end: 7 }" + - "Return { value: 4 }" + want_result: {"a": 11, "b": 12, "c": 13} + + - note: nested_filtering_and_transform + description: Filter then transform (chained operations) + example_rego: "[x * 2 | x = numbers[_]; x > 5]" + literals: + - [3, 7, 4, 9, 2, 8] + - 5 + - 2 + instruction_params: + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 1 + value_reg: 2 + result_reg: 6 + body_start: 5 + loop_end: 10 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "ArrayNew { dest: 5 }" + - "Load { dest: 9, literal_idx: 1 }" + - "Load { dest: 10, literal_idx: 2 }" + - "LoopStart { params_index: 0 }" + - "Gt { dest: 3, left: 2, right: 9 }" + - "AssertCondition { condition: 3 }" + - "Mul { dest: 4, left: 2, right: 10 }" + - "ArrayPush { arr: 5, value: 4 }" + - "LoopNext { body_start: 5, loop_end: 10 }" + - "Return { value: 5 }" + want_result: [14, 18, 16] + + - note: multi_field_validation + description: Validate multiple fields meet criteria + example_rego: "valid { user.age >= 18; user.verified == true; user.status == \"active\" }" + literals: + - {"age": 25, "verified": true, "status": "active"} + - "age" + - 18 + - "verified" + - "status" + - "active" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + # Check age >= 18 + - "Load { dest: 1, literal_idx: 1 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Load { dest: 3, literal_idx: 2 }" + - "Ge { dest: 4, left: 2, right: 3 }" + - "Load { dest: 5, literal_idx: 3 }" + - "Index { dest: 6, container: 0, key: 5 }" + - "LoadTrue { dest: 7 }" + - "Eq { dest: 8, left: 6, right: 7 }" + - "And { dest: 9, left: 4, right: 8 }" + - "Load { dest: 10, literal_idx: 4 }" + - "Index { dest: 11, container: 0, key: 10 }" + - "Load { dest: 12, literal_idx: 5 }" + - "Eq { dest: 13, left: 11, right: 12 }" + - "And { dest: 14, left: 9, right: 13 }" + - "Return { value: 14 }" + want_result: true + + - note: multi_field_validation_failure + description: Multi-field validation fails when one criterion not met + example_rego: "valid { user.age >= 18; user.verified == true; user.status == \"active\" }" + literals: + - {"age": 25, "verified": false, "status": "active"} + - "age" + - 18 + - "verified" + - "status" + - "active" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + # Check age >= 18 + - "Load { dest: 1, literal_idx: 1 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Load { dest: 3, literal_idx: 2 }" + - "Ge { dest: 4, left: 2, right: 3 }" + - "Load { dest: 5, literal_idx: 3 }" + - "Index { dest: 6, container: 0, key: 5 }" + - "LoadTrue { dest: 7 }" + - "Eq { dest: 8, left: 6, right: 7 }" + - "And { dest: 9, left: 4, right: 8 }" + - "Load { dest: 10, literal_idx: 4 }" + - "Index { dest: 11, container: 0, key: 10 }" + - "Load { dest: 12, literal_idx: 5 }" + - "Eq { dest: 13, left: 11, right: 12 }" + - "And { dest: 14, left: 9, right: 13 }" + - "Return { value: 14 }" + want_result: false + + - note: set_membership_check + description: Check if value is in allowed set + example_rego: "allowed { input.action in {\"read\", \"write\", \"delete\"} }" + literals: + - {"action": "read"} + - "action" + - "read" + - "write" + - "delete" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Index { dest: 2, container: 0, key: 1 }" + # Create allowed set + - "SetNew { dest: 3 }" + - "Load { dest: 4, literal_idx: 2 }" + - "SetAdd { set: 3, value: 4 }" + - "Load { dest: 5, literal_idx: 3 }" + - "SetAdd { set: 3, value: 5 }" + - "Load { dest: 6, literal_idx: 4 }" + - "SetAdd { set: 3, value: 6 }" + # Check membership + - "Contains { dest: 7, collection: 3, value: 2 }" + - "Return { value: 7 }" + want_result: true + + - note: set_membership_denied + description: Check fails when value not in allowed set + example_rego: "allowed { input.action in {\"read\", \"write\", \"delete\"} }" + literals: + - {"action": "execute"} + - "action" + - "read" + - "write" + - "delete" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Index { dest: 2, container: 0, key: 1 }" + # Create allowed set + - "SetNew { dest: 3 }" + - "Load { dest: 4, literal_idx: 2 }" + - "SetAdd { set: 3, value: 4 }" + - "Load { dest: 5, literal_idx: 3 }" + - "SetAdd { set: 3, value: 5 }" + - "Load { dest: 6, literal_idx: 4 }" + - "SetAdd { set: 3, value: 6 }" + # Check membership + - "Contains { dest: 7, collection: 3, value: 2 }" + - "Return { value: 7 }" + want_result: false + + - note: aggregation_count + description: Count items matching condition + example_rego: "count([x | x = items[_]; x.status == \"active\"])" + literals: + - [{"id": 1, "status": "active"}, {"id": 2, "status": "inactive"}, {"id": 3, "status": "active"}] + - "status" + - "active" + instruction_params: + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 1 + value_reg: 2 + result_reg: 8 + body_start: 4 + loop_end: 10 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "ArrayNew { dest: 6 }" + - "Load { dest: 9, literal_idx: 2 }" + - "LoopStart { params_index: 0 }" + - "Load { dest: 3, literal_idx: 1 }" + - "Index { dest: 4, container: 2, key: 3 }" + - "Eq { dest: 5, left: 4, right: 9 }" + - "AssertCondition { condition: 5 }" + - "ArrayPush { arr: 6, value: 2 }" + - "LoopNext { body_start: 4, loop_end: 10 }" + - "Return { value: 6 }" + want_result: [{"id": 1, "status": "active"}, {"id": 3, "status": "active"}] + + - note: hierarchical_permission_check + description: Check nested permission structure + example_rego: "allow { data.permissions[input.user][input.resource] == \"allow\" }" + literals: + - {"permissions": {"alice": {"doc1": "allow", "doc2": "deny"}, "bob": {"doc1": "deny"}}} + - {"user": "alice", "resource": "doc1"} + - "permissions" + - "user" + - "resource" + - "allow" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + # Load permissions + - "Load { dest: 2, literal_idx: 2 }" + - "Index { dest: 3, container: 0, key: 2 }" + # Load user + - "Load { dest: 4, literal_idx: 3 }" + - "Index { dest: 5, container: 1, key: 4 }" + # Index permissions by user + - "Index { dest: 6, container: 3, key: 5 }" + # Load resource + - "Load { dest: 7, literal_idx: 4 }" + - "Index { dest: 8, container: 1, key: 7 }" + # Index user permissions by resource + - "Index { dest: 9, container: 6, key: 8 }" + # Check if "allow" + - "Load { dest: 10, literal_idx: 5 }" + - "Eq { dest: 11, left: 9, right: 10 }" + - "Return { value: 11 }" + want_result: true + + - note: default_value_pattern + description: Provide default value when path undefined + example_rego: "timeout := data.config.timeout; timeout == 30 if not data.config.timeout" + literals: + - {} + - "config" + - "timeout" + - 30 + instruction_params: + loop_params: + - mode: "Any" + collection: 7 + key_reg: 8 + value_reg: 9 + result_reg: 10 + body_start: 10 + loop_end: 13 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Load { dest: 3, literal_idx: 2 }" + - "Index { dest: 4, container: 2, key: 3 }" + - "Load { dest: 5, literal_idx: 3 }" + - "Contains { dest: 6, collection: 2, value: 3 }" + - "ArrayNew { dest: 7 }" + - "ArrayPush { arr: 7, value: 6 }" + - "LoopStart { params_index: 0 }" + - "AssertCondition { condition: 9 }" + - "Move { dest: 5, src: 4 }" + - "LoopNext { body_start: 10, loop_end: 13 }" + - "Return { value: 5 }" + want_result: 30 diff --git a/tests/rvm/vm/suites/interpreter_operator_compatibility.yaml b/tests/rvm/vm/suites/interpreter_operator_compatibility.yaml new file mode 100644 index 0000000..4912ca0 --- /dev/null +++ b/tests/rvm/vm/suites/interpreter_operator_compatibility.yaml @@ -0,0 +1,351 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Interpreter Operator Compatibility Test Suite +# Tests that VM operators behave exactly like interpreter operators +# Focuses on edge cases like undefined value handling, division by zero, etc. + +cases: + # Undefined Value Arithmetic Tests + - note: undefined_add + description: Test addition with undefined value + example_rego: "input.nonexistent + 5" + input: {} # Empty input to create undefined access + literals: + - "nonexistent" + - 5 + instructions: + - "LoadInput { dest: 0 }" # Load input into register 0 + - "Load { dest: 1, literal_idx: 0 }" # Load string "nonexistent" for indexing + - "Load { dest: 2, literal_idx: 1 }" # Load 5 into register 2 + - "Index { dest: 3, container: 0, key: 1 }" # Access input.nonexistent (undefined) + - "Add { dest: 4, left: 3, right: 2 }" # undefined + 5 + - "Return { value: 4 }" + want_result: "#undefined" + + - note: undefined_sub + description: Test subtraction with undefined value + example_rego: "input.nonexistent - 5" + input: {} + literals: + - "nonexistent" + - 5 + instructions: + - "LoadInput { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Index { dest: 3, container: 0, key: 1 }" + - "Sub { dest: 4, left: 3, right: 2 }" + - "Return { value: 4 }" + want_result: "#undefined" + + - note: undefined_mul + description: Test multiplication with undefined value + example_rego: "input.nonexistent * 5" + input: {} + literals: + - "nonexistent" + - 5 + instructions: + - "LoadInput { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Index { dest: 3, container: 0, key: 1 }" + - "Mul { dest: 4, left: 3, right: 2 }" + - "Return { value: 4 }" + want_result: "#undefined" + + # Division by Zero Tests + - note: division_by_zero + description: Test division by zero returns undefined (non-strict mode) + example_rego: "5 / 0" + literals: + - 5 + - 0 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Div { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" + + - note: modulo_by_zero + description: Test modulo by zero returns undefined (non-strict mode) + example_rego: "5 % 0" + literals: + - 5 + - 0 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Mod { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" + + # Modulo Float Tests + - note: modulo_float + description: Test modulo with floating point numbers (should error) + example_rego: "5.5 % 2" + literals: + - 5.5 + - 2 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Mod { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_error: "modulo on floating-point number" + + # Undefined Comparison Tests + - note: undefined_eq + description: Test equality comparison with undefined value + example_rego: "input.nonexistent == 5" + input: {} + literals: + - "nonexistent" + - 5 + instructions: + - "LoadInput { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Index { dest: 3, container: 0, key: 1 }" + - "Eq { dest: 4, left: 3, right: 2 }" + - "Return { value: 4 }" + want_result: "#undefined" + + - note: undefined_ne + description: Test inequality comparison with undefined value + example_rego: "input.nonexistent != 5" + input: {} + literals: + - "nonexistent" + - 5 + instructions: + - "LoadInput { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Index { dest: 3, container: 0, key: 1 }" + - "Ne { dest: 4, left: 3, right: 2 }" + - "Return { value: 4 }" + want_result: "#undefined" + + - note: undefined_lt + description: Test less than comparison with undefined value + example_rego: "input.nonexistent < 5" + input: {} + literals: + - "nonexistent" + - 5 + instructions: + - "LoadInput { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Index { dest: 3, container: 0, key: 1 }" + - "Lt { dest: 4, left: 3, right: 2 }" + - "Return { value: 4 }" + want_result: "#undefined" + + - note: undefined_le + description: Test less than or equal comparison with undefined value + example_rego: "input.nonexistent <= 5" + input: {} + literals: + - "nonexistent" + - 5 + instructions: + - "LoadInput { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Index { dest: 3, container: 0, key: 1 }" + - "Le { dest: 4, left: 3, right: 2 }" + - "Return { value: 4 }" + want_result: "#undefined" + + - note: undefined_gt + description: Test greater than comparison with undefined value + example_rego: "input.nonexistent > 5" + input: {} + literals: + - "nonexistent" + - 5 + instructions: + - "LoadInput { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Index { dest: 3, container: 0, key: 1 }" + - "Gt { dest: 4, left: 3, right: 2 }" + - "Return { value: 4 }" + want_result: "#undefined" + + - note: undefined_ge + description: Test greater than or equal comparison with undefined value + example_rego: "input.nonexistent >= 5" + input: {} + literals: + - "nonexistent" + - 5 + instructions: + - "LoadInput { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Index { dest: 3, container: 0, key: 1 }" + - "Ge { dest: 4, left: 3, right: 2 }" + - "Return { value: 4 }" + want_result: "#undefined" + + # Number Type Precision Tests + - note: number_add_precision + description: Test Number type addition preserves precision + example_rego: "1.1 + 2.2" + literals: + - 1.1 + - 2.2 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Add { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: 3.3 + + - note: number_sub_precision + description: Test Number type subtraction preserves precision + example_rego: "5.5 - 2.2" + literals: + - 5.5 + - 2.2 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Sub { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: 3.3 + + - note: number_mul_precision + description: Test Number type multiplication preserves precision + example_rego: "2.5 * 4.0" + literals: + - 2.5 + - 4.0 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Mul { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: 10.0 + + - note: number_div_precision + description: Test Number type division preserves precision + example_rego: "7.5 / 2.5" + literals: + - 7.5 + - 2.5 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Div { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: 3.0 + + # Integer Modulo Tests + - note: integer_modulo + description: Test integer modulo works correctly + example_rego: "7 % 3" + literals: + - 7 + - 3 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Mod { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: 1 + + # Value Ordering Tests + - note: value_ordering_null_bool + description: Test null < bool ordering + example_rego: "null < true" + literals: + - null + - true + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Lt { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: true + + - note: value_ordering_bool_number + description: Test bool < number ordering + example_rego: "true < 1" + literals: + - true + - 1 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Lt { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: true + + - note: value_ordering_number_string + description: Test number < string ordering + example_rego: "1 < \"a\"" + literals: + - 1 + - "a" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Lt { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: true + + # Edge Cases for Mixed Undefined Operations + - note: undefined_both_operands + description: Test operation with both operands undefined + example_rego: "input.nonexistent1 + input.nonexistent2" + input: {} + literals: + - "nonexistent1" + - "nonexistent2" + instructions: + - "LoadInput { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Index { dest: 3, container: 0, key: 1 }" + - "Index { dest: 4, container: 0, key: 2 }" + - "Add { dest: 5, left: 3, right: 4 }" + - "Return { value: 5 }" + want_result: "#undefined" + + - note: undefined_right_operand_arithmetic + description: Test arithmetic with right operand undefined + example_rego: "5 + input.nonexistent" + input: {} + literals: + - 5 + - "nonexistent" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "LoadInput { dest: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Index { dest: 3, container: 1, key: 2 }" + - "Add { dest: 4, left: 0, right: 3 }" + - "Return { value: 4 }" + want_result: "#undefined" + + - note: undefined_right_operand_comparison + description: Test comparison with right operand undefined + example_rego: "5 == input.nonexistent" + input: {} + literals: + - 5 + - "nonexistent" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "LoadInput { dest: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Index { dest: 3, container: 1, key: 2 }" + - "Eq { dest: 4, left: 0, right: 3 }" + - "Return { value: 4 }" + want_result: "#undefined" diff --git a/tests/rvm/vm/suites/invalid_collection_ops.yaml b/tests/rvm/vm/suites/invalid_collection_ops.yaml new file mode 100644 index 0000000..4a0887b --- /dev/null +++ b/tests/rvm/vm/suites/invalid_collection_ops.yaml @@ -0,0 +1,61 @@ +# Invalid Collection Operations Test Suite +# Exercises error conditions for collection-specific instructions. + +cases: + - note: object_set_on_non_object + description: ObjectSet on non-object register triggers error + literals: + - 1 + - "key" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "ObjectSet { obj: 0, key: 1, value: 1 }" + want_error: "Register 0 does not contain an object" + + - note: array_push_on_non_array + description: ArrayPush on scalar register fails + literals: + - 1 + - 2 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "ArrayPush { arr: 0, value: 1 }" + want_error: "Register 0 does not contain an array" + + - note: set_add_on_non_set + description: SetAdd on scalar register fails + literals: + - 1 + - 2 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "SetAdd { set: 0, value: 1 }" + want_error: "Register 0 does not contain a set" + + - note: object_create_invalid_template + description: ObjectCreate fails when template literal is not object + literals: + - [] + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + instructions: + - "ObjectCreate { params_index: 0 }" + want_error: "ObjectCreate: template is not an object" + + - note: contains_on_scalar_returns_false + description: Contains on scalar should return false + literals: + - 1 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Contains { dest: 2, collection: 0, value: 1 }" + - "Return { value: 2 }" + want_result: false diff --git a/tests/rvm/vm/suites/load_data_input.yaml b/tests/rvm/vm/suites/load_data_input.yaml new file mode 100644 index 0000000..29114bb --- /dev/null +++ b/tests/rvm/vm/suites/load_data_input.yaml @@ -0,0 +1,250 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Load Data and Input Test Suite +# Tests LoadData and LoadInput instructions with various edge cases +# Covers empty, nested, undefined values, and interaction with virtual data lookups + +cases: + - note: load_data_empty + description: LoadData from empty data context + example_rego: "data" + data: {} + literals: [] + instructions: + - "LoadData { dest: 0 }" + - "Return { value: 0 }" + want_result: {} + + - note: load_input_empty + description: LoadInput from empty input context + example_rego: "input" + input: {} + literals: [] + instructions: + - "LoadInput { dest: 0 }" + - "Return { value: 0 }" + want_result: {} + + - note: load_data_with_values + description: LoadData returns populated data object + example_rego: "data" + data: {} + literals: [] + # This would require the test harness to support setting data + # For now, testing structure only + instructions: + - "LoadData { dest: 0 }" + - "Return { value: 0 }" + want_result: {} # Default empty since harness may not set data + + - note: load_input_with_values + description: LoadInput returns populated input object + example_rego: "input" + input: {} + literals: [] + instructions: + - "LoadInput { dest: 0 }" + - "Return { value: 0 }" + want_result: {} # Default empty + + - note: load_data_and_index + description: LoadData followed by indexing + example_rego: "data.users" + data: {} + literals: + - "users" + instructions: + - "LoadData { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" # users doesn't exist in empty data + + - note: load_input_and_index + description: LoadInput followed by indexing + example_rego: "input.user.name" + input: {} + literals: + - "user" + - "name" + instructions: + - "LoadInput { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Load { dest: 3, literal_idx: 1 }" + - "Index { dest: 4, container: 2, key: 3 }" + - "Return { value: 4 }" + want_result: "#undefined" # user doesn't exist in empty input + + - note: load_data_multiple_times + description: LoadData can be called multiple times + example_rego: "data == data" + data: {} + literals: [] + instructions: + - "LoadData { dest: 0 }" + - "LoadData { dest: 1 }" + - "Eq { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: true + + - note: load_input_multiple_times + description: LoadInput can be called multiple times + example_rego: "input == input" + input: {} + literals: [] + instructions: + - "LoadInput { dest: 0 }" + - "LoadInput { dest: 1 }" + - "Eq { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: true + + - note: load_data_not_equal_to_input + description: data and input are separate contexts + example_rego: "data == input" + data: {} + input: {} + literals: [] + instructions: + - "LoadData { dest: 0 }" + - "LoadInput { dest: 1 }" + - "Eq { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: true # Both empty {} + + - note: load_data_in_loop + description: LoadData inside loop body + example_rego: "[ data | _ = [1, 2][_] ]" + data: {} + literals: + - 1 + - 2 + instruction_params: + array_create_params: + - dest: 0 + elements: [1, 2] + loop_params: + - mode: ForEach + collection: 0 + key_reg: 3 + value_reg: 4 + result_reg: 7 + body_start: 5 + loop_end: 8 + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "ArrayCreate { params_index: 0 }" + - "ArrayNew { dest: 6 }" + - "LoopStart { params_index: 0 }" + # Loop body starts + - "LoadData { dest: 5 }" + - "ArrayPush { arr: 6, value: 5 }" + - "LoopNext { body_start: 5, loop_end: 8 }" + # Loop done + - "Return { value: 6 }" + want_result: [{}, {}] + + - note: load_input_in_loop + description: LoadInput inside loop body + example_rego: "[ input | _ = [1, 2][_] ]" + input: {} + literals: + - 1 + - 2 + instruction_params: + array_create_params: + - dest: 0 + elements: [1, 2] + loop_params: + - mode: ForEach + collection: 0 + key_reg: 3 + value_reg: 4 + result_reg: 7 + body_start: 5 + loop_end: 8 + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "ArrayCreate { params_index: 0 }" + - "ArrayNew { dest: 6 }" + - "LoopStart { params_index: 0 }" + # Loop body starts + - "LoadInput { dest: 5 }" + - "ArrayPush { arr: 6, value: 5 }" + - "LoopNext { body_start: 5, loop_end: 8 }" + # Loop done + - "Return { value: 6 }" + want_result: [{}, {}] + + - note: load_data_in_arithmetic + description: Using LoadData result in arithmetic should fail + example_rego: "data + 1" + data: {} + literals: + - 1 + instructions: + - "LoadData { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Add { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_error: "Cannot add Object({}) and Number(1)" + + - note: load_input_in_comparison + description: Compare input to literal object + example_rego: "input == {}" + input: {} + literals: + - {} + instructions: + - "LoadInput { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Eq { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: true + + - note: load_data_conditional + description: LoadData in conditional check + example_rego: "data.flag" + data: {} + literals: + - "flag" + instructions: + - "LoadData { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "LoadFalse { dest: 3 }" + - "Or { dest: 4, left: 2, right: 3 }" + - "Return { value: 4 }" + want_result: "#undefined" # flag is undefined + + - note: load_data_chained_index + description: Deep path indexing on data + example_rego: "data.a.b.c.d" + data: {} + literals: + - "a" + - "b" + - "c" + - "d" + instruction_params: + chained_index_params: + - dest: 5 + root: 0 + path_components: + - register: 1 + - register: 2 + - register: 3 + - register: 4 + instructions: + - "LoadData { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Load { dest: 3, literal_idx: 2 }" + - "Load { dest: 4, literal_idx: 3 }" + - "ChainedIndex { params_index: 0 }" + - "Return { value: 5 }" + want_result: "#undefined" diff --git a/tests/rvm/vm/suites/loop_invalid_iteration.yaml b/tests/rvm/vm/suites/loop_invalid_iteration.yaml new file mode 100644 index 0000000..3904507 --- /dev/null +++ b/tests/rvm/vm/suites/loop_invalid_iteration.yaml @@ -0,0 +1,43 @@ +# Loop Invalid Iteration Test Suite +# Asserts loop error paths and instruction limit behavior. + +cases: + - note: loop_start_on_scalar + description: LoopStart over scalar treats collection as empty and leaves result false + literals: + - 1 + instruction_params: + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 1 + value_reg: 2 + result_reg: 3 + body_start: 2 + loop_end: 2 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "LoopStart { params_index: 0 }" + - "Return { value: 3 }" + want_result: false + + - note: instruction_limit_exceeded + description: VM stops once max instruction limit reached + literals: + - [1, 2] + instruction_params: + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 1 + value_reg: 2 + result_reg: 3 + body_start: 2 + loop_end: 4 + max_instructions: 3 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "LoopStart { params_index: 0 }" + - "LoopNext { body_start: 2, loop_end: 4 }" + - "Return { value: 3 }" + want_error: "exceeded maximum instruction limit" diff --git a/tests/rvm/vm/suites/loops/array_comprehensions.yaml b/tests/rvm/vm/suites/loops/array_comprehensions.yaml new file mode 100644 index 0000000..cc9a33b --- /dev/null +++ b/tests/rvm/vm/suites/loops/array_comprehensions.yaml @@ -0,0 +1,160 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Array Comprehension Test Suite +# Tests array comprehensions - collect transformed values based on conditions +# Corresponds to Rego's "[transform | condition]" patterns + +cases: + - note: array_simple_transform + description: Simple array comprehension with transformation + example_rego: | + # Transform array elements by doubling them + [x * 2 | x := [1, 2, 3][_]] # [2, 4, 6] + literals: + - 1 + - 2 + - 3 + - 2 # multiplier + instruction_params: + comprehension_begin_params: + - mode: "Array" + collection_reg: 7 + key_reg: 4 + value_reg: 5 + body_start: 8 + comprehension_end: 11 + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 8 + body_start: 9 + loop_end: 12 + instructions: + - "ArrayNew { dest: 0 }" # Create input array [1, 2, 3] in register 0 + - "Load { dest: 1, literal_idx: 0 }" # Load 1 into register 1 + - "ArrayPush { arr: 0, value: 1 }" # Push 1 to array + - "Load { dest: 2, literal_idx: 1 }" # Load 2 into register 2 + - "ArrayPush { arr: 0, value: 2 }" # Push 2 to array + - "Load { dest: 3, literal_idx: 2 }" # Load 3 into register 3 + - "ArrayPush { arr: 0, value: 3 }" # Push 3 to array + - "ComprehensionBegin { params_index: 0 }" # Start array comprehension and initialize result in register 7 + - "LoopStart { params_index: 0 }" # Start loop over array elements + - "Load { dest: 6, literal_idx: 3 }" # Load multiplier 2 into register 6 + - "Mul { dest: 9, left: 5, right: 6 }" # Multiply current value by 2, store result in register 9 + - "ComprehensionYield { value_reg: 9 }" # Add transformed value to comprehension + - "LoopNext { body_start: 9, loop_end: 12 }" # Continue to next iteration or exit + - "Return { value: 7 }" # Return comprehension collection + want_result: [2, 4, 6] + + - note: array_empty_input + description: Array comprehension with empty input + example_rego: | + # Transform empty array + [x + 5 | x := [][_]] # [] (empty array) + literals: + - 5 # addend + instruction_params: + comprehension_begin_params: + - mode: "Array" + collection_reg: 7 + key_reg: 4 + value_reg: 5 + body_start: 2 + comprehension_end: 6 + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 8 + body_start: 3 + loop_end: 6 + instructions: + - "ArrayNew { dest: 0 }" # Create empty input array in register 0 + - "ComprehensionBegin { params_index: 0 }" # Start array comprehension and initialize result in register 7 + - "LoopStart { params_index: 0 }" # Start loop over array elements + - "Load { dest: 6, literal_idx: 0 }" # Load addend 5 into register 6 + - "Add { dest: 8, left: 5, right: 6 }" # Add 5 to current value, store result in register 8 + - "ComprehensionYield { value_reg: 8 }" # Add transformed value to comprehension + - "LoopNext { body_start: 3, loop_end: 6 }" # Continue to next iteration or exit + - "Return { value: 7 }" # Return comprehension collection + want_result: [] + + - note: array_single_element + description: Array comprehension with single element + example_rego: | + # Transform single element array + [x - 1 | x := [10][_]] # [9] + literals: + - 10 + - 1 # subtrahend + instruction_params: + comprehension_begin_params: + - mode: "Array" + collection_reg: 7 + key_reg: 4 + value_reg: 5 + body_start: 4 + comprehension_end: 8 + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 8 + body_start: 5 + loop_end: 8 + instructions: + - "ArrayNew { dest: 0 }" # Create input array [10] in register 0 + - "Load { dest: 1, literal_idx: 0 }" # Load 10 into register 1 + - "ArrayPush { arr: 0, value: 1 }" # Push 10 to array + - "ComprehensionBegin { params_index: 0 }" # Start array comprehension and initialize result in register 7 + - "LoopStart { params_index: 0 }" # Start loop over array elements + - "Load { dest: 6, literal_idx: 1 }" # Load subtrahend 1 into register 6 + - "Sub { dest: 9, left: 5, right: 6 }" # Subtract 1 from current value, store result in register 9 + - "ComprehensionYield { value_reg: 9 }" # Add transformed value to comprehension + - "LoopNext { body_start: 5, loop_end: 8 }" # Continue to next iteration or exit + - "Return { value: 7 }" # Return comprehension collection + want_result: [9] + + - note: array_with_null_values + description: Array comprehension with null value handling + example_rego: | + # Process array with null values - nulls are preserved + [x | x := [1, null, 3][_]] # [1, null, 3] + literals: + - 1 + - 3 + instruction_params: + comprehension_begin_params: + - mode: "Array" + collection_reg: 7 + key_reg: 4 + value_reg: 5 + body_start: 8 + comprehension_end: 11 + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 8 + body_start: 9 + loop_end: 11 + instructions: + - "ArrayNew { dest: 0 }" # Create input array in register 0 + - "Load { dest: 1, literal_idx: 0 }" # Load 1 + - "ArrayPush { arr: 0, value: 1 }" # Push 1 to array + - "LoadNull { dest: 2 }" # Load null value + - "ArrayPush { arr: 0, value: 2 }" # Push null to array + - "Load { dest: 3, literal_idx: 1 }" # Load 3 + - "ArrayPush { arr: 0, value: 3 }" # Push 3 to array + - "ComprehensionBegin { params_index: 0 }" # Start array comprehension and initialize result in register 7 + - "LoopStart { params_index: 0 }" # Start loop over array elements + - "ComprehensionYield { value_reg: 5 }" # Add current value (including null) to comprehension + - "LoopNext { body_start: 9, loop_end: 11 }" # Continue to next iteration or exit + - "Return { value: 7 }" # Return comprehension collection + want_result: [1, null, 3] diff --git a/tests/rvm/vm/suites/loops/empty.yaml b/tests/rvm/vm/suites/loops/empty.yaml new file mode 100644 index 0000000..539ca92 --- /dev/null +++ b/tests/rvm/vm/suites/loops/empty.yaml @@ -0,0 +1,295 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Empty Collections Loop Test Suite +# Tests behavior of various loop types over empty collections +# 1. Comprehensions should evaluate to their empty versions +# 2. some..in should evaluate to false +# 3. every should evaluate to true + +cases: + - note: existential_empty_array + description: Existential quantification (some) on empty array should return false + example_rego: | + # some x in [] + # x > 0 # false - no elements to satisfy condition + literals: + - {} + - 0 # comparison value + instruction_params: + loop_params: + - mode: "Any" + collection: 0 + key_reg: 4 + value_reg: 5 + body_start: 2 + loop_end: 6 + result_reg: 6 + instructions: + - "ArrayNew { dest: 0 }" # Create empty array in register 0 + - "LoopStart { params_index: 0 }" + - "Load { dest: 7, literal_idx: 1 }" # Load comparison value 0 + - "Gt { dest: 8, left: 5, right: 7 }" # Check if current value > 0 + - "AssertCondition { condition: 8 }" # Assert the condition + - "LoopNext { body_start: 2, loop_end: 6 }" + - "Return { value: 6 }" # Return false for empty collection + want_result: false + + - note: universal_empty_array + description: Universal quantification (every) on empty array should return true + example_rego: | + # every x in [] + # x > 0 # true - vacuously true (no elements to violate condition) + literals: + - {} + - 0 # comparison value + instruction_params: + loop_params: + - mode: "Every" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 6 + body_start: 2 + loop_end: 6 + instructions: + - "ArrayNew { dest: 0 }" # Create empty array in register 0 + - "LoopStart { params_index: 0 }" + - "Load { dest: 7, literal_idx: 1 }" # Load comparison value 0 + - "Gt { dest: 8, left: 5, right: 7 }" # Check if current value > 0 + - "AssertCondition { condition: 8 }" # Assert the condition + - "LoopNext { body_start: 2, loop_end: 6 }" + - "Return { value: 6 }" # Return true for empty collection (vacuously true) + want_result: true + + - note: array_comprehension_empty + description: Array comprehension on empty collection should return empty array + example_rego: | + # [x + 1 | x = []; true] # [] + # Transform each element by adding 1 + literals: + - {} + - 1 # value to add + instruction_params: + comprehension_begin_params: + - mode: "Array" + collection_reg: 1 + key_reg: 4 + value_reg: 5 + body_start: 3 + comprehension_end: 9 + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 8 + body_start: 4 + loop_end: 9 + instructions: + - "ArrayNew { dest: 0 }" # Create empty input array in register 0 + - "ComprehensionBegin { params_index: 0 }" # Start array comprehension + - "LoopStart { params_index: 0 }" # Start loop over array elements + - "Load { dest: 6, literal_idx: 1 }" # Load 1 into register 6 + - "Add { dest: 7, left: 5, right: 6 }" # Add 1 to current value + - "ComprehensionYield { value_reg: 7 }" # Add result to comprehension + - "LoadBool { dest: 8, value: true }" # Load true (condition always passes) + - "AssertCondition { condition: 8 }" # Assert true condition + - "LoopNext { body_start: 4, loop_end: 9 }" # Continue to next iteration or exit + - "Return { value: 1 }" # Return the result array (should be empty) + want_result: [] + + - note: set_comprehension_empty + description: Set comprehension on empty collection should return empty set + example_rego: | + # {x + 1 | x = []; true} # set() + # Transform each element by adding 1 into a set + literals: + - {} + - 1 # value to add + instruction_params: + comprehension_begin_params: + - mode: "Set" + collection_reg: 1 + key_reg: 4 + value_reg: 5 + body_start: 3 + comprehension_end: 9 + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 8 + body_start: 4 + loop_end: 9 + instructions: + - "ArrayNew { dest: 0 }" # Create empty input array in register 0 + - "ComprehensionBegin { params_index: 0 }" # Start set comprehension + - "LoopStart { params_index: 0 }" # Start loop over array elements + - "Load { dest: 6, literal_idx: 1 }" # Load 1 into register 6 + - "Add { dest: 7, left: 5, right: 6 }" # Add 1 to current value + - "ComprehensionYield { value_reg: 7 }" # Add result to comprehension + - "LoadBool { dest: 8, value: true }" # Load true (condition always passes) + - "AssertCondition { condition: 8 }" # Assert true condition + - "LoopNext { body_start: 4, loop_end: 9 }" # Continue to next iteration or exit + - "Return { value: 1 }" # Return the result set (should be empty) + want_result: + set!: [] # Set serializes as empty array + + - note: object_comprehension_empty + description: Object comprehension on empty collection should return empty object + example_rego: | + # {k: v + 1 | some k, v in {}; true} # {} + # Transform each key-value pair + literals: + - {} + - 1 # value to add + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + comprehension_begin_params: + - mode: "Object" + collection_reg: 1 + key_reg: 4 + value_reg: 5 + body_start: 3 + comprehension_end: 9 + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 8 + body_start: 4 + loop_end: 9 + instructions: + - "ObjectCreate { params_index: 0 }" + - "ComprehensionBegin { params_index: 0 }" # Start object comprehension + - "LoopStart { params_index: 0 }" # Start loop over object elements + - "Load { dest: 6, literal_idx: 1 }" # Load 1 into register 6 + - "Add { dest: 7, left: 5, right: 6 }" # Add 1 to current value + - "ComprehensionYield { value_reg: 7 }" # Add result to comprehension (object comprehension needs special handling) + - "LoadBool { dest: 8, value: true }" # Load true (condition always passes) + - "AssertCondition { condition: 8 }" # Assert true condition + - "LoopNext { body_start: 4, loop_end: 9 }" # Continue to next iteration or exit + - "Return { value: 1 }" # Return the result object (should be empty) + want_result: {} + + - note: existential_empty_set + description: Existential quantification on empty set should return false + example_rego: | + # some x in set() + # x > 0 # false - no elements in set + literals: + - {} + - 0 # comparison value + instruction_params: + loop_params: + - mode: "Any" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 6 + body_start: 2 + loop_end: 6 + instructions: + - "SetNew { dest: 0 }" # Create empty set in register 0 + - "LoopStart { params_index: 0 }" + - "Load { dest: 7, literal_idx: 1 }" # Load comparison value 0 + - "Gt { dest: 8, left: 5, right: 7 }" # Check if current value > 0 + - "AssertCondition { condition: 8 }" # Assert the condition + - "LoopNext { body_start: 2, loop_end: 6 }" + - "Return { value: 6 }" # Return false for empty set + want_result: false + + - note: universal_empty_object + description: Universal quantification on empty object should return true + example_rego: | + # every k, v in {} + # v > 0 # true - vacuously true (no key-value pairs to violate condition) + literals: + - {} + - 0 # comparison value + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + loop_params: + - mode: "Every" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 6 + body_start: 2 + loop_end: 6 + instructions: + - "ObjectCreate { params_index: 0 }" + - "LoopStart { params_index: 0 }" + - "Load { dest: 7, literal_idx: 1 }" # Load comparison value 0 + - "Gt { dest: 8, left: 5, right: 7 }" # Check if current value > 0 + - "AssertCondition { condition: 8 }" # Assert the condition + - "LoopNext { body_start: 2, loop_end: 6 }" + - "Return { value: 6 }" # Return true for empty object (vacuously true) + want_result: true + + - note: nested_empty_comprehensions + description: Nested comprehensions with empty collections + example_rego: | + # [[y | y = []; true] | x = []; true] # [] + # Nested array comprehension where both inner and outer collections are empty + literals: [] + instruction_params: + comprehension_begin_params: + - mode: "Array" + collection_reg: 0 + key_reg: 2 + value_reg: 3 + body_start: 3 + comprehension_end: 16 + - mode: "Array" + collection_reg: 6 + key_reg: 8 + value_reg: 9 + body_start: 7 + comprehension_end: 12 + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 2 + value_reg: 3 + result_reg: 10 + body_start: 4 + loop_end: 16 + - mode: "ForEach" + collection: 6 + key_reg: 8 + value_reg: 9 + result_reg: 11 + body_start: 8 + loop_end: 12 + instructions: + - "ArrayNew { dest: 0 }" # Create empty outer array in register 0 + - "ArrayNew { dest: 1 }" # Create empty result array in register 1 + - "ComprehensionBegin { params_index: 0 }" + - "LoopStart { params_index: 0 }" # Start outer loop + # Inner array comprehension (for each x in outer empty array) + - "ArrayNew { dest: 6 }" # Create empty inner array in register 6 + - "ArrayNew { dest: 7 }" # Create result for inner comprehension in register 7 + - "ComprehensionBegin { params_index: 1 }" + - "LoopStart { params_index: 1 }" # Start inner loop + - "ComprehensionYield { value_reg: 9 }" # Push inner value to inner result (never executes) + - "LoadBool { dest: 10, value: true }" # Load true + - "AssertCondition { condition: 10 }" # Assert true condition for inner loop + - "LoopNext { body_start: 8, loop_end: 12 }" # Continue inner loop + - "ComprehensionYield { value_reg: 7 }" # Push inner result to outer result + - "LoadBool { dest: 11, value: true }" # Load true + - "AssertCondition { condition: 11 }" # Assert true condition for outer loop + - "LoopNext { body_start: 4, loop_end: 16 }" # Continue outer loop + - "Return { value: 1 }" # Return the nested result (should be empty) + want_result: [] diff --git a/tests/rvm/vm/suites/loops/existential.yaml b/tests/rvm/vm/suites/loops/existential.yaml new file mode 100644 index 0000000..b4de163 --- /dev/null +++ b/tests/rvm/vm/suites/loops/existential.yaml @@ -0,0 +1,174 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Existential Loops Test Suite (some) +# Tests existential quantification loops - succeed if ANY element satisfies the condition +# Corresponds to Rego's "some x in collection; condition" patterns + +cases: + - note: existential_basic_some + description: Basic existential quantification - some element satisfies condition + example_rego: | + # Check if any element in array is greater than 2 + some x in [1, 2, 3] + x > 2 # true (3 > 2) + literals: + - 1 + - 2 + - 3 + - 2 # comparison value + instruction_params: + loop_params: + - mode: "Existential" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 6 + body_start: 8 + loop_end: 12 + instructions: + - "ArrayNew { dest: 0 }" # Create input array [1, 2, 3] in register 0 + - "Load { dest: 1, literal_idx: 0 }" # Load 1 into register 1 + - "ArrayPush { arr: 0, value: 1 }" # Push 1 to array + - "Load { dest: 2, literal_idx: 1 }" # Load 2 into register 2 + - "ArrayPush { arr: 0, value: 2 }" # Push 2 to array + - "Load { dest: 3, literal_idx: 2 }" # Load 3 into register 3 + - "ArrayPush { arr: 0, value: 3 }" # Push 3 to array + - "LoopStart { params_index: 0 }" # Start existential loop using parameter table index 0 + - "Load { dest: 7, literal_idx: 3 }" # Load comparison value 2 into register 7 + - "Gt { dest: 8, left: 5, right: 7 }" # Check if current value > 2 + - "AssertCondition { condition: 8 }" # Assert the condition result for existential logic + - "LoopNext { body_start: 8, loop_end: 12 }" # Continue to next iteration or exit early if condition met + - "Return { value: 6 }" # Return result (true if any element satisfied condition) + want_result: true + + - note: existential_none_satisfy + description: Existential quantification where no element satisfies condition + example_rego: | + # Check if any element in array is greater than 5 + some x in [1, 2] + x > 5 # false (no element > 5) + literals: + - 1 + - 2 + - 5 # comparison value + instruction_params: + loop_params: + - mode: "Existential" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 6 + body_start: 6 + loop_end: 10 + instructions: + - "ArrayNew { dest: 0 }" # Create input array [1, 2] in register 0 + - "Load { dest: 1, literal_idx: 0 }" # Load 1 into register 1 + - "ArrayPush { arr: 0, value: 1 }" # Push 1 to array + - "Load { dest: 2, literal_idx: 1 }" # Load 2 into register 2 + - "ArrayPush { arr: 0, value: 2 }" # Push 2 to array + - "LoopStart { params_index: 0 }" # Start existential loop using parameter table index 0 + - "Load { dest: 7, literal_idx: 2 }" # Load comparison value 5 into register 7 + - "Gt { dest: 8, left: 5, right: 7 }" # Check if current value > 5 + - "AssertCondition { condition: 8 }" # Assert the condition result for existential logic + - "LoopNext { body_start: 6, loop_end: 10 }" # Continue to next iteration + - "Return { value: 6 }" # Return result (false since no element satisfied condition) + want_result: false + + - note: existential_empty_collection + description: Existential quantification on empty collection + example_rego: | + # Check if any element in empty array satisfies condition + some x in [] + x > 0 # false (no elements to check) + literals: + - 0 # comparison value + instruction_params: + loop_params: + - mode: "Existential" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 6 + body_start: 2 + loop_end: 6 + instructions: + - "ArrayNew { dest: 0 }" # Create empty input array in register 0 + - "LoopStart { params_index: 0 }" # Start existential loop using parameter table index 0 + - "Load { dest: 7, literal_idx: 0 }" # Load comparison value 0 into register 7 + - "Gt { dest: 8, left: 5, right: 7 }" # Check if current value > 0 + - "AssertCondition { condition: 8 }" # Assert the condition result for existential logic + - "LoopNext { body_start: 2, loop_end: 6 }" # Continue to next iteration + - "Return { value: 6 }" # Return result (false for empty collection) + want_result: false + + - note: existential_simplified_arrays + description: Existential quantification with simple array test + example_rego: | + # Check if any element in array is greater than 5 + # Simplified version: check if [3, 7, 4] contains element > 5 + some x in [3, 7, 4] + x > 5 # true (7 > 5) + literals: + - 3 + - 7 + - 4 + - 5 # comparison value + instruction_params: + loop_params: + - mode: "Existential" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 6 + body_start: 8 + loop_end: 12 + instructions: + - "ArrayNew { dest: 0 }" # Create array [3, 7, 4] in register 0 + - "Load { dest: 1, literal_idx: 0 }" # Load 3 + - "ArrayPush { arr: 0, value: 1 }" # Push 3 to array + - "Load { dest: 2, literal_idx: 1 }" # Load 7 + - "ArrayPush { arr: 0, value: 2 }" # Push 7 to array + - "Load { dest: 3, literal_idx: 2 }" # Load 4 + - "ArrayPush { arr: 0, value: 3 }" # Push 4 to array + - "LoopStart { params_index: 0 }" # Start existential loop using parameter table index 0 + - "Load { dest: 7, literal_idx: 3 }" # Load comparison value 5 + - "Gt { dest: 8, left: 5, right: 7 }" # Check if current value > 5 + - "AssertCondition { condition: 8 }" # Assert the condition for existential logic + - "LoopNext { body_start: 8, loop_end: 12 }" # Continue to next iteration + - "Return { value: 6 }" # Return result + want_result: true + + - note: some_basic_failure + description: Basic existential loop that fails + example_rego: "some x in [1, 2, 3]; x > 5" # false because no element > 5 + literals: + - 1 + - 2 + - 3 + - 5 # comparison value + instruction_params: + loop_params: + - mode: "Existential" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 6 + body_start: 8 + loop_end: 12 + instructions: + - "ArrayNew { dest: 0 }" # Create array [1, 2, 3] in register 0 + - "Load { dest: 1, literal_idx: 0 }" # Load 1 into register 1 + - "ArrayPush { arr: 0, value: 1 }" # Push 1 to array + - "Load { dest: 2, literal_idx: 1 }" # Load 2 into register 2 + - "ArrayPush { arr: 0, value: 2 }" # Push 2 to array + - "Load { dest: 3, literal_idx: 2 }" # Load 3 into register 3 + - "ArrayPush { arr: 0, value: 3 }" # Push 3 to array + - "LoopStart { params_index: 0 }" # Start existential loop using parameter table index 0 + - "Load { dest: 7, literal_idx: 3 }" # Load comparison value 5 into register 7 + - "Gt { dest: 8, left: 5, right: 7 }" # Check if current value > 5, store result in register 8 + - "AssertCondition { condition: 8 }" # Assert the condition (fails for all elements) + - "LoopNext { body_start: 8, loop_end: 12 }" # Continue to next iteration or exit + - "Return { value: 6 }" # Return boolean result from loop + want_result: false + diff --git a/tests/rvm/vm/suites/loops/loop_comprehension_interactions.yaml b/tests/rvm/vm/suites/loops/loop_comprehension_interactions.yaml new file mode 100644 index 0000000..04cab0c --- /dev/null +++ b/tests/rvm/vm/suites/loops/loop_comprehension_interactions.yaml @@ -0,0 +1,223 @@ +# Advanced Loop and Comprehension Interaction Suite +# Exercises nested quantifiers inside comprehensions and object key/value emission. + +cases: + - note: array_comprehension_filters_with_inner_any + description: Array comprehension keeps only members whose nested array passes an Any loop + example_rego: | + [arr | + arr := [[1, 0], [1, 2]][_]; + some v in arr; v == 0 + ] + literals: + - 1 + - 0 + - 2 + instruction_params: + comprehension_begin_params: + - mode: "Array" + collection_reg: 7 + result_reg: 7 + key_reg: 10 + value_reg: 11 + body_start: 14 + comprehension_end: 23 + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 10 + value_reg: 11 + result_reg: 12 + body_start: 15 + loop_end: 23 + - mode: "Any" + collection: 11 + key_reg: 13 + value_reg: 14 + result_reg: 15 + body_start: 16 + loop_end: 21 + instructions: + - "ArrayNew { dest: 0 }" + - "ArrayNew { dest: 1 }" + - "Load { dest: 2, literal_idx: 0 }" + - "ArrayPush { arr: 1, value: 2 }" + - "Load { dest: 3, literal_idx: 1 }" + - "ArrayPush { arr: 1, value: 3 }" + - "ArrayPush { arr: 0, value: 1 }" + - "ArrayNew { dest: 4 }" + - "Load { dest: 5, literal_idx: 0 }" + - "ArrayPush { arr: 4, value: 5 }" + - "Load { dest: 6, literal_idx: 2 }" + - "ArrayPush { arr: 4, value: 6 }" + - "ArrayPush { arr: 0, value: 4 }" + - "ComprehensionBegin { params_index: 0 }" + - "LoopStart { params_index: 0 }" + - "LoopStart { params_index: 1 }" + - "Load { dest: 16, literal_idx: 1 }" + - "Eq { dest: 17, left: 14, right: 16 }" + - "AssertCondition { condition: 17 }" + - "ComprehensionYield { value_reg: 11 }" + - "LoopNext { body_start: 16, loop_end: 21 }" + - "LoopNext { body_start: 15, loop_end: 23 }" + - "ComprehensionEnd" + - "Return { value: 7 }" + want_result: + - [1, 0] + + - note: array_comprehension_requires_inner_every + description: Array comprehension keeps only members whose nested array passes an Every loop + example_rego: | + [arr | + arr := [[1, 1], [-1, 2], [2, 3]][_]; + every v in arr; v > 0 + ] + literals: + - 1 + - -1 + - 2 + - 3 + - 0 + instruction_params: + comprehension_begin_params: + - mode: "Array" + collection_reg: 9 + result_reg: 9 + key_reg: 10 + value_reg: 11 + body_start: 20 + comprehension_end: 29 + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 10 + value_reg: 11 + result_reg: 12 + body_start: 20 + loop_end: 29 + - mode: "ForEach" + collection: 11 + key_reg: 13 + value_reg: 14 + result_reg: 18 + body_start: 22 + loop_end: 26 + instructions: + - "ArrayNew { dest: 0 }" + - "ArrayNew { dest: 1 }" + - "Load { dest: 2, literal_idx: 0 }" + - "ArrayPush { arr: 1, value: 2 }" + - "ArrayPush { arr: 1, value: 2 }" + - "ArrayPush { arr: 0, value: 1 }" + - "ArrayNew { dest: 3 }" + - "Load { dest: 4, literal_idx: 1 }" + - "ArrayPush { arr: 3, value: 4 }" + - "Load { dest: 5, literal_idx: 2 }" + - "ArrayPush { arr: 3, value: 5 }" + - "ArrayPush { arr: 0, value: 3 }" + - "ArrayNew { dest: 6 }" + - "Load { dest: 7, literal_idx: 2 }" + - "ArrayPush { arr: 6, value: 7 }" + - "Load { dest: 8, literal_idx: 3 }" + - "ArrayPush { arr: 6, value: 8 }" + - "ArrayPush { arr: 0, value: 6 }" + - "ComprehensionBegin { params_index: 0 }" + - "LoopStart { params_index: 0 }" + - "LoadTrue { dest: 15 }" + - "LoopStart { params_index: 1 }" + - "Load { dest: 16, literal_idx: 4 }" + - "Gt { dest: 17, left: 14, right: 16 }" + - "And { dest: 15, left: 15, right: 17 }" + - "LoopNext { body_start: 22, loop_end: 26 }" + - "AssertCondition { condition: 15 }" + - "ComprehensionYield { value_reg: 11 }" + - "LoopNext { body_start: 20, loop_end: 29 }" + - "ComprehensionEnd" + - "Return { value: 9 }" + want_result: + - [1, 1] + - [2, 3] + + - note: object_comprehension_with_nested_any + description: Object comprehension emits keys only when nested Any loop succeeds + example_rego: | + { entry[0]: true | + entry := [["a", [1, 2]], ["b", [1]]][_]; + some v in entry[1]; v % 2 == 0 + } + literals: + - {} + - "a" + - 1 + - 2 + - "b" + - 0 + instruction_params: + object_create_params: + - dest: 12 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + comprehension_begin_params: + - mode: "Object" + collection_reg: 12 + result_reg: 12 + key_reg: 17 + value_reg: 22 + body_start: 21 + comprehension_end: 36 + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 13 + value_reg: 14 + result_reg: 15 + body_start: 22 + loop_end: 36 + - mode: "Any" + collection: 19 + key_reg: 20 + value_reg: 21 + result_reg: 22 + body_start: 27 + loop_end: 33 + instructions: + - "ArrayNew { dest: 0 }" + - "ArrayNew { dest: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "ArrayPush { arr: 1, value: 2 }" + - "ArrayNew { dest: 3 }" + - "Load { dest: 4, literal_idx: 2 }" + - "ArrayPush { arr: 3, value: 4 }" + - "Load { dest: 5, literal_idx: 3 }" + - "ArrayPush { arr: 3, value: 5 }" + - "ArrayPush { arr: 1, value: 3 }" + - "ArrayPush { arr: 0, value: 1 }" + - "ArrayNew { dest: 6 }" + - "Load { dest: 7, literal_idx: 4 }" + - "ArrayPush { arr: 6, value: 7 }" + - "ArrayNew { dest: 8 }" + - "Load { dest: 9, literal_idx: 2 }" + - "ArrayPush { arr: 8, value: 9 }" + - "ArrayPush { arr: 6, value: 8 }" + - "ArrayPush { arr: 0, value: 6 }" + - "ObjectCreate { params_index: 0 }" + - "ComprehensionBegin { params_index: 0 }" + - "LoopStart { params_index: 0 }" + - "Load { dest: 16, literal_idx: 5 }" + - "Index { dest: 17, container: 14, key: 16 }" + - "Load { dest: 18, literal_idx: 2 }" + - "Index { dest: 19, container: 14, key: 18 }" + - "LoopStart { params_index: 1 }" + - "Load { dest: 23, literal_idx: 3 }" + - "Mod { dest: 24, left: 21, right: 23 }" + - "Load { dest: 25, literal_idx: 5 }" + - "Eq { dest: 26, left: 24, right: 25 }" + - "AssertCondition { condition: 26 }" + - "LoopNext { body_start: 27, loop_end: 33 }" + - "AssertCondition { condition: 22 }" + - "ComprehensionYield { value_reg: 22, key_reg: 17 }" + - "LoopNext { body_start: 22, loop_end: 36 }" + - "ComprehensionEnd" + - "Return { value: 12 }" + want_result: {"a": true} diff --git a/tests/rvm/vm/suites/loops/nested.yaml b/tests/rvm/vm/suites/loops/nested.yaml new file mode 100644 index 0000000..f7ee545 --- /dev/null +++ b/tests/rvm/vm/suites/loops/nested.yaml @@ -0,0 +1,183 @@ +name: "Nested Loops Test Suite" +description: "Test various combinations and levels of nesting for different looping constructs" + +cases: + - note: "simple_nested_comprehension" + description: "Test simple nested array comprehension" + example_rego: | + [[x | x := [1, 2][_]] | _ := [1, 2][_]] + literals: + - 1 + - 2 + instruction_params: + comprehension_begin_params: + - mode: "Array" + collection_reg: 1 + key_reg: 2 + value_reg: 3 + body_start: 6 + comprehension_end: 23 + - mode: "Array" + collection_reg: 11 + key_reg: 12 + value_reg: 13 + body_start: 12 + comprehension_end: 20 + loop_params: + - mode: "ForEach" + collection: 6 + key_reg: 7 + value_reg: 8 + result_reg: 9 + body_start: 7 + loop_end: 22 + - mode: "ForEach" + collection: 16 + key_reg: 17 + value_reg: 18 + result_reg: 19 + body_start: 15 + loop_end: 18 + instructions: + - "ComprehensionBegin { params_index: 0 }" # array comprehension in r1, body: 4-21 (P0) + - "Load { dest: 4, literal_idx: 0 }" # Load literal: 1 + - "Load { dest: 5, literal_idx: 1 }" # Load literal: 2 + - "ArrayNew { dest: 6 }" # Create empty array r6 + - "ArrayPush { arr: 6, value: 4 }" # Push r4 to r6 + - "ArrayPush { arr: 6, value: 5 }" # Push r5 to r6 -> [1, 2] + - "LoopStart { params_index: 0 }" # foreach loop over r6, body: 8-20 (P0) + # Outer loop body starts here (index 7) + - "Move { dest: 10, src: 8 }" # Copy value from r8 to r10 + - "ComprehensionBegin { params_index: 1 }" # array comprehension in r11, body: 10-18 (P1) + - "Load { dest: 14, literal_idx: 0 }" # Load literal: 1 + - "Load { dest: 15, literal_idx: 1 }" # Load literal: 2 + - "ArrayNew { dest: 16 }" # Create empty array r16 + - "ArrayPush { arr: 16, value: 14 }" # Push r14 to r16 + - "ArrayPush { arr: 16, value: 15 }" # Push r15 to r16 -> [1, 2] + - "LoopStart { params_index: 1 }" # foreach loop over r16, body: 14-17 (P1) + # Inner loop body starts here (index 16) + - "Move { dest: 20, src: 18 }" # Copy value from r18 to r20 + - "ComprehensionYield { value_reg: 20 }" # Yield value to comprehension + - "LoopNext { body_start: 16, loop_end: 19 }" # continue → 16 or exit → 19 + # Inner comprehension end (index 19) + - "ComprehensionEnd" # End comprehension block + - "ComprehensionYield { value_reg: 11 }" # Yield value to comprehension + - "LoopNext { body_start: 7, loop_end: 22 }" # continue → 7 or exit → 22 + # Outer comprehension end (index 22) + - "ComprehensionEnd" # End comprehension block + - "Move { dest: 0, src: 1 }" # Copy value from r1 to r0 + - "Return { value: 0 }" # Return value from r0 + want_result: [[1, 2], [1, 2]] + + - note: "some_nested_comprehension" + description: "Test some with nested array comprehension" + example_rego: | + [1, 2][_] == [x | x := [1, 2, 3][_]; x > 1][_] + literals: + - 1 + - 2 + - 3 + instruction_params: + comprehension_begin_params: + - mode: "Array" + collection_reg: 12 + result_reg: 12 + key_reg: 10 + value_reg: 11 + body_start: 16 + comprehension_end: 20 + loop_params: + - mode: "Any" + collection: 0 + key_reg: 7 + value_reg: 8 + result_reg: 9 + body_start: 14 + loop_end: 27 + - mode: "ForEach" + collection: 3 + key_reg: 10 + value_reg: 11 + result_reg: 21 + body_start: 16 + loop_end: 20 + - mode: "Any" + collection: 12 + key_reg: 13 + value_reg: 14 + result_reg: 15 + body_start: 22 + loop_end: 25 + instructions: + - "ArrayNew { dest: 0 }" # Index 0: Build left array [1, 2] + - "Load { dest: 1, literal_idx: 0 }" # Index 1: Load literal 1 + - "ArrayPush { arr: 0, value: 1 }" # Index 2 + - "Load { dest: 2, literal_idx: 1 }" # Index 3: Load literal 2 + - "ArrayPush { arr: 0, value: 2 }" # Index 4 + - "ArrayNew { dest: 3 }" # Index 5: Build source array [1, 2, 3] + - "Load { dest: 4, literal_idx: 0 }" # Index 6 + - "ArrayPush { arr: 3, value: 4 }" # Index 7 + - "Load { dest: 5, literal_idx: 1 }" # Index 8 + - "ArrayPush { arr: 3, value: 5 }" # Index 9 + - "Load { dest: 6, literal_idx: 2 }" # Index 10 + - "ArrayPush { arr: 3, value: 6 }" # Index 11 + - "Load { dest: 16, literal_idx: 0 }" # Index 12: Load 1 for comparison (stays stable) + - "LoopStart { params_index: 0 }" # Index 13: Start outer some loop over [1, 2] + - "ComprehensionBegin { params_index: 0 }" # Index 14: Build filtered comprehension + - "LoopStart { params_index: 1 }" # Index 15: Iterate source values for comprehension + - "Gt { dest: 17, left: 11, right: 16 }" # Index 16: Check x > 1 + - "AssertCondition { condition: 17 }" # Index 17: Skip values <= 1 + - "ComprehensionYield { value_reg: 11 }" # Index 18: Include qualifying value + - "LoopNext { body_start: 16, loop_end: 20 }" # Index 19: Continue comprehension loop + - "ComprehensionEnd" # Index 20: Finish comprehension block + - "LoopStart { params_index: 2 }" # Index 21: Iterate comprehension results + - "Eq { dest: 18, left: 8, right: 14 }" # Index 22: Compare outer value with result value + - "AssertCondition { condition: 18 }" # Index 23: Success when values match + - "LoopNext { body_start: 22, loop_end: 25 }" # Index 24: Continue inner any loop + - "AssertCondition { condition: 15 }" # Index 25: Require some match from inner any loop + - "LoopNext { body_start: 14, loop_end: 27 }" # Index 26: Continue outer some loop + - "Return { value: 9 }" # Index 27 + want_result: true + + - note: "nested_with_condition" + description: "Test nested loops with conditional logic" + example_rego: | + [x | x := [1, 2, 3][_]; x > 1] + literals: + - 1 + - 2 + - 3 + instruction_params: + comprehension_begin_params: + - mode: "Array" + collection_reg: 6 + result_reg: 6 + key_reg: 4 + value_reg: 5 + body_start: 9 + comprehension_end: 14 + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 7 + body_start: 9 + loop_end: 14 + instructions: + - "ArrayNew { dest: 0 }" # Index 0: Create input array [1, 2, 3] + - "Load { dest: 1, literal_idx: 0 }" # Index 1: Load 1 + - "ArrayPush { arr: 0, value: 1 }" # Index 2: Push 1 to array + - "Load { dest: 2, literal_idx: 1 }" # Index 3: Load 2 + - "ArrayPush { arr: 0, value: 2 }" # Index 4: Push 2 to array + - "Load { dest: 3, literal_idx: 2 }" # Index 5: Load 3 + - "ArrayPush { arr: 0, value: 3 }" # Index 6: Push 3 to array + - "ComprehensionBegin { params_index: 0 }" # Index 7: Start comprehension + - "LoopStart { params_index: 0 }" # Index 8: Start loop + - "Load { dest: 7, literal_idx: 0 }" # Index 9: Load 1 for comparison + - "Gt { dest: 8, left: 5, right: 7 }" # Index 10: x > 1 + - "AssertCondition { condition: 8 }" # Index 11: Assert x > 1 (skip if false) + - "ComprehensionYield { value_reg: 5 }" # Index 12: Push x to result if condition true + - "LoopNext { body_start: 9, loop_end: 13 }" # Index 13: Continue loop + - "Return { value: 6 }" # Index 14: Return comprehension result + want_result: [2, 3] diff --git a/tests/rvm/vm/suites/loops/nested_fixed.yaml b/tests/rvm/vm/suites/loops/nested_fixed.yaml new file mode 100644 index 0000000..f9603cb --- /dev/null +++ b/tests/rvm/vm/suites/loops/nested_fixed.yaml @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Placeholder for nested fixed loops - currently empty +# TODO: Add test cases for fixed nested loop scenarios + +cases: [] diff --git a/tests/rvm/vm/suites/loops/object_comprehensions.yaml b/tests/rvm/vm/suites/loops/object_comprehensions.yaml new file mode 100644 index 0000000..5f37c00 --- /dev/null +++ b/tests/rvm/vm/suites/loops/object_comprehensions.yaml @@ -0,0 +1,192 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Object Comprehension Test Suite +# Tests object comprehensions - construct objects with key-value pairs based on conditions +# Corresponds to Rego's "{key: value | condition}" patterns + +cases: + - note: object_simple_key_value + description: Simple object comprehension with computed values + example_rego: | + # Create object mapping each value to its double + {x: x * 2 | x := [1, 2, 3][_]} # {1: 2, 2: 4, 3: 6} + literals: + - {} + - 1 + - 2 + - 3 + - 2 # multiplier + instruction_params: + object_create_params: + - dest: 9 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + comprehension_begin_params: + - mode: "Object" + collection_reg: 9 + result_reg: 9 + key_reg: 4 + value_reg: 5 + body_start: 10 + comprehension_end: 13 + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 10 + body_start: 10 + loop_end: 14 + instructions: + - "ArrayNew { dest: 0 }" # Index 0: Create input array [1, 2, 3] + - "Load { dest: 1, literal_idx: 1 }" # Index 1: Load 1 + - "ArrayPush { arr: 0, value: 1 }" # Index 2 + - "Load { dest: 2, literal_idx: 2 }" # Index 3: Load 2 + - "ArrayPush { arr: 0, value: 2 }" # Index 4 + - "Load { dest: 3, literal_idx: 3 }" # Index 5: Load 3 + - "ArrayPush { arr: 0, value: 3 }" # Index 6 + - "ObjectCreate { params_index: 0 }" # Index 7: Create empty result object in r9 + - "ComprehensionBegin { params_index: 0 }" # Index 8: Start object comprehension + - "LoopStart { params_index: 0 }" # Index 9: Start loop + - "Load { dest: 11, literal_idx: 4 }" # Index 10: Load multiplier 2 + - "Mul { dest: 12, left: 5, right: 11 }" # Index 11: Multiply value by 2 + - "ComprehensionYield { value_reg: 12, key_reg: 5 }" # Index 12: Add key-value pair + - "LoopNext { body_start: 10, loop_end: 14 }" # Index 13: Continue loop + - "Return { value: 9 }" # Index 14: Return result object + want_result: {1: 2, 2: 4, 3: 6} + + - note: object_empty_input + description: Object comprehension with empty input + example_rego: | + # Create object from empty array + {x: x + 5 | x := [][_]} # {} (empty object) + literals: + - {} + - 5 # addend + instruction_params: + object_create_params: + - dest: 9 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + comprehension_begin_params: + - mode: "Object" + collection_reg: 9 + result_reg: 9 + key_reg: 4 + value_reg: 5 + body_start: 4 + comprehension_end: 7 + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 10 + body_start: 4 + loop_end: 8 + instructions: + - "ArrayNew { dest: 0 }" # Index 0: Create empty input array + - "ObjectCreate { params_index: 0 }" # Index 1: Create empty result object in r9 + - "ComprehensionBegin { params_index: 0 }" # Index 2: Start object comprehension + - "LoopStart { params_index: 0 }" # Index 3: Start loop + - "Load { dest: 10, literal_idx: 1 }" # Index 4: Load addend 5 + - "Add { dest: 11, left: 5, right: 10 }" # Index 5: Add 5 to value + - "ComprehensionYield { value_reg: 11, key_reg: 5 }" # Index 6: Add key-value pair + - "LoopNext { body_start: 4, loop_end: 8 }" # Index 7: Continue loop + - "Return { value: 9 }" # Index 8: Return result object + want_result: {} + + - note: object_single_element + description: Object comprehension with single element + example_rego: | + # Create object with single key-value pair + {x: x - 1 | x := [5][_]} # {5: 4} + literals: + - {} + - 5 + - 1 # subtrahend + instruction_params: + object_create_params: + - dest: 9 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + comprehension_begin_params: + - mode: "Object" + collection_reg: 9 + result_reg: 9 + key_reg: 4 + value_reg: 5 + body_start: 6 + comprehension_end: 9 + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 10 + body_start: 6 + loop_end: 10 + instructions: + - "ArrayNew { dest: 0 }" # Index 0: Create input array [5] + - "Load { dest: 1, literal_idx: 1 }" # Index 1: Load 5 + - "ArrayPush { arr: 0, value: 1 }" # Index 2: Push 5 to array + - "ObjectCreate { params_index: 0 }" # Index 3: Create empty result object in r9 + - "ComprehensionBegin { params_index: 0 }" # Index 4: Start object comprehension + - "LoopStart { params_index: 0 }" # Index 5: Start loop + - "Load { dest: 10, literal_idx: 2 }" # Index 6: Load subtrahend 1 + - "Sub { dest: 11, left: 5, right: 10 }" # Index 7: Subtract 1 from value + - "ComprehensionYield { value_reg: 11, key_reg: 5 }" # Index 8: Add key-value pair + - "LoopNext { body_start: 6, loop_end: 10 }" # Index 9: Continue loop + - "Return { value: 9 }" # Index 10: Return result object + want_result: {5: 4} + + - note: object_with_null_keys + description: Object comprehension with null keys and values + example_rego: | + # Create object with null keys/values + {x: x | x := [1, null, 2][_]} # {1: 1, null: null, 2: 2} + literals: + - {} + - 1 + - 2 + instruction_params: + object_create_params: + - dest: 9 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + comprehension_begin_params: + - mode: "Object" + collection_reg: 9 + result_reg: 9 + key_reg: 4 + value_reg: 5 + body_start: 10 + comprehension_end: 11 + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 10 + body_start: 10 + loop_end: 12 + instructions: + - "ArrayNew { dest: 0 }" # Index 0: Create input array + - "Load { dest: 1, literal_idx: 1 }" # Index 1: Load 1 + - "ArrayPush { arr: 0, value: 1 }" # Index 2: Push 1 to array + - "LoadNull { dest: 2 }" # Index 3: Load null value + - "ArrayPush { arr: 0, value: 2 }" # Index 4: Push null to array + - "Load { dest: 3, literal_idx: 2 }" # Index 5: Load 2 + - "ArrayPush { arr: 0, value: 3 }" # Index 6: Push 2 to array + - "ObjectCreate { params_index: 0 }" # Index 7: Create empty result object in r9 + - "ComprehensionBegin { params_index: 0 }" # Index 8: Start object comprehension + - "LoopStart { params_index: 0 }" # Index 9: Start loop + - "ComprehensionYield { value_reg: 5, key_reg: 5 }" # Index 10: Use value as both key and value + - "LoopNext { body_start: 10, loop_end: 12 }" # Index 11: Continue loop + - "Return { value: 9 }" # Index 12: Return result object + want_result: {1: 1, null: null, 2: 2} diff --git a/tests/rvm/vm/suites/loops/set_comprehensions.yaml b/tests/rvm/vm/suites/loops/set_comprehensions.yaml new file mode 100644 index 0000000..88eef4e --- /dev/null +++ b/tests/rvm/vm/suites/loops/set_comprehensions.yaml @@ -0,0 +1,139 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Set Comprehension Test Suite +# Tests set comprehensions - collect unique transformed values based on conditions +# Corresponds to Rego's "{transform | condition}" patterns + +cases: + - note: set_simple_transform + description: Simple set comprehension with transformation + example_rego: | + # Transform array values into set - duplicates removed + {x * 2 | x := [1, 2, 2, 3][_]} # {2, 4, 6} (duplicates removed) + literals: + - 1 + - 2 + - 3 + - 2 # multiplier + instruction_params: + comprehension_start_params: + - mode: "Set" + collection_reg: 0 + key_reg: 4 + value_reg: 5 + result_reg: 7 + body_start: 10 + comprehension_end: 14 + instructions: + - "ArrayNew { dest: 0 }" # Create input array [1, 2, 2, 3] in register 0 + - "Load { dest: 1, literal_idx: 0 }" # Load 1 into register 1 + - "ArrayPush { arr: 0, value: 1 }" # Push 1 to array + - "Load { dest: 2, literal_idx: 1 }" # Load 2 into register 2 + - "ArrayPush { arr: 0, value: 2 }" # Push 2 to array + - "ArrayPush { arr: 0, value: 2 }" # Push 2 again to array (duplicate) + - "Load { dest: 3, literal_idx: 2 }" # Load 3 into register 3 + - "ArrayPush { arr: 0, value: 3 }" # Push 3 to array + - "SetNew { dest: 7 }" # Initialize result set in register 7 + - "ComprehensionStart { params_index: 0 }" # Start set comprehension + - "Load { dest: 6, literal_idx: 3 }" # Load multiplier 2 into register 6 + - "Mul { dest: 10, left: 5, right: 6 }" # Multiply current value by 2, store result in register 10 + - "ComprehensionAdd { value_reg: 10 }" # Add transformed value to result set (auto-deduplicates) + - "Halt" # End comprehension + - "Return { value: 7 }" # Return result set + want_result: + set!: [2, 4, 6] + + - note: set_empty_input + description: Set comprehension with empty input + example_rego: | + # Transform empty array into set + {x + 5 | x := [][_]} # {} (empty set) + literals: + - 5 # addend + instruction_params: + comprehension_start_params: + - mode: "Set" + collection_reg: 0 + key_reg: 4 + value_reg: 5 + result_reg: 7 + body_start: 3 + comprehension_end: 7 + instructions: + - "ArrayNew { dest: 0 }" # Create empty input array in register 0 + - "SetNew { dest: 7 }" # Initialize result set in register 7 + - "ComprehensionStart { params_index: 0 }" # Start set comprehension + - "Load { dest: 6, literal_idx: 0 }" # Load addend 5 into register 6 + - "Add { dest: 10, left: 5, right: 6 }" # Add 5 to current value, store result in register 10 + - "ComprehensionAdd { value_reg: 10 }" # Add transformed value to result set + - "Halt" # End comprehension + - "Return { value: 7 }" # Return result set + want_result: + set!: [] + + - note: set_single_element + description: Set comprehension with single element + example_rego: | + # Transform single element into set + {x - 1 | x := [10][_]} # {9} + literals: + - 10 + - 1 # subtrahend + instruction_params: + comprehension_start_params: + - mode: "Set" + collection_reg: 0 + key_reg: 4 + value_reg: 5 + result_reg: 7 + body_start: 5 + comprehension_end: 9 + instructions: + - "ArrayNew { dest: 0 }" # Create input array [10] in register 0 + - "Load { dest: 1, literal_idx: 0 }" # Load 10 into register 1 + - "ArrayPush { arr: 0, value: 1 }" # Push 10 to array + - "SetNew { dest: 7 }" # Initialize result set in register 7 + - "ComprehensionStart { params_index: 0 }" # Start set comprehension + - "Load { dest: 6, literal_idx: 1 }" # Load subtrahend 1 into register 6 + - "Sub { dest: 10, left: 5, right: 6 }" # Subtract 1 from current value, store result in register 10 + - "ComprehensionAdd { value_reg: 10 }" # Add transformed value to result set + - "Halt" # End comprehension + - "Return { value: 7 }" # Return result set + want_result: + set!: [9] + + - note: set_with_null_deduplication + description: Set comprehension with null values and deduplication + example_rego: | + # Collect unique values including nulls + {x | x := [1, null, 1, null, 2][_]} # {1, null, 2} + literals: + - 1 + - 2 + instruction_params: + comprehension_start_params: + - mode: "Set" + collection_reg: 0 + key_reg: 4 + value_reg: 5 + result_reg: 7 + body_start: 11 + comprehension_end: 13 + instructions: + - "ArrayNew { dest: 0 }" # Create input array in register 0 + - "Load { dest: 1, literal_idx: 0 }" # Load 1 + - "ArrayPush { arr: 0, value: 1 }" # Push 1 to array + - "LoadNull { dest: 2 }" # Load null value + - "ArrayPush { arr: 0, value: 2 }" # Push null to array + - "ArrayPush { arr: 0, value: 1 }" # Push 1 again (duplicate) + - "ArrayPush { arr: 0, value: 2 }" # Push null again (duplicate) + - "Load { dest: 3, literal_idx: 1 }" # Load 2 + - "ArrayPush { arr: 0, value: 3 }" # Push 2 to array + - "SetNew { dest: 7 }" # Initialize result set in register 7 + - "ComprehensionStart { params_index: 0 }" # Start set comprehension + - "ComprehensionAdd { value_reg: 5 }" # Add current value to result set (auto-deduplicates) + - "Halt" # End comprehension + - "Return { value: 7 }" # Return result set + want_result: + set!: [1, null, 2] diff --git a/tests/rvm/vm/suites/loops/universal.yaml b/tests/rvm/vm/suites/loops/universal.yaml new file mode 100644 index 0000000..eddc80d --- /dev/null +++ b/tests/rvm/vm/suites/loops/universal.yaml @@ -0,0 +1,143 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Universal Loops Test Suite +# Tests universal quantification - succeed if ALL elements satisfy the condition +# Corresponds to Rego's "every x in collection; condition" patterns + +cases: + - note: universal_basic_every + description: Basic universal quantification - every element satisfies condition + example_rego: | + # Check if every element in array is greater than 0 + every x in [1, 2, 3] { + x > 0 # true (all elements > 0) + } + literals: + - 1 + - 2 + - 3 + - 0 # comparison value + instruction_params: + loop_params: + - mode: "Every" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 6 + body_start: 8 + loop_end: 12 + instructions: + - "ArrayNew { dest: 0 }" # Create input array [1, 2, 3] in register 0 + - "Load { dest: 1, literal_idx: 0 }" # Load 1 into register 1 + - "ArrayPush { arr: 0, value: 1 }" # Push 1 to array + - "Load { dest: 2, literal_idx: 1 }" # Load 2 into register 2 + - "ArrayPush { arr: 0, value: 2 }" # Push 2 to array + - "Load { dest: 3, literal_idx: 2 }" # Load 3 into register 3 + - "ArrayPush { arr: 0, value: 3 }" # Push 3 to array + - "LoopStart { params_index: 0 }" # Start universal loop using parameter table index 0 + - "Load { dest: 7, literal_idx: 3 }" # Load comparison value 0 into register 7 + - "Gt { dest: 8, left: 5, right: 7 }" # Check if current value > 0 + - "AssertCondition { condition: 8 }" # Assert the condition result for universal logic + - "LoopNext { body_start: 8, loop_end: 12 }" # Continue to next iteration or exit early if condition fails + - "Return { value: 6 }" # Return result (true if all elements satisfied condition) + want_result: true + + - note: universal_one_fails + description: Universal quantification where one element fails condition + example_rego: | + # Check if every element in array is greater than 1 + every x in [1, 2, 3] { + x > 1 # false (1 is not > 1) + } + literals: + - 1 + - 2 + - 3 + - 1 # comparison value + instruction_params: + loop_params: + - mode: "Every" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 6 + body_start: 8 + loop_end: 12 + instructions: + - "ArrayNew { dest: 0 }" # Create input array [1, 2, 3] in register 0 + - "Load { dest: 1, literal_idx: 0 }" # Load 1 into register 1 + - "ArrayPush { arr: 0, value: 1 }" # Push 1 to array + - "Load { dest: 2, literal_idx: 1 }" # Load 2 into register 2 + - "ArrayPush { arr: 0, value: 2 }" # Push 2 to array + - "Load { dest: 3, literal_idx: 2 }" # Load 3 into register 3 + - "ArrayPush { arr: 0, value: 3 }" # Push 3 to array + - "LoopStart { params_index: 0 }" # Start universal loop using parameter table index 0 + - "Load { dest: 7, literal_idx: 3 }" # Load comparison value 1 into register 7 + - "Gt { dest: 8, left: 5, right: 7 }" # Check if current value > 1 + - "AssertCondition { condition: 8 }" # Assert the condition result for universal logic + - "LoopNext { body_start: 8, loop_end: 12 }" # Continue to next iteration or exit early on failure + - "Return { value: 6 }" # Return result (false since first element failed condition) + want_result: false + + - note: universal_empty_collection + description: Universal quantification on empty collection + example_rego: | + # Check if every element in empty array satisfies condition + every x in [] { + x > 0 # true (vacuously true - all 0 elements satisfy condition) + } + literals: + - 0 # comparison value + instruction_params: + loop_params: + - mode: "Every" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 6 + body_start: 2 + loop_end: 5 + instructions: + - "ArrayNew { dest: 0 }" # Create empty input array in register 0 + - "LoopStart { params_index: 0 }" # Start universal loop + - "Load { dest: 7, literal_idx: 0 }" # Load comparison value 0 into register 7 + - "Gt { dest: 8, left: 5, right: 7 }" # Check if current value > 0 + - "LoopNext { body_start: 2, loop_end: 5 }" # Continue to next iteration + - "Return { value: 6 }" # Return result (true for empty collection - vacuous truth) + want_result: true + + - note: universal_null_handling + description: Universal quantification with null values + example_rego: | + # Test behavior with null values - should handle gracefully + every x in [2, null, 4] { + x != null # false (null fails the condition) + } + literals: + - 2 + - 4 + instruction_params: + loop_params: + - mode: "Every" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 6 + body_start: 8 + loop_end: 12 + instructions: + - "ArrayNew { dest: 0 }" # Create input array in register 0 + - "Load { dest: 1, literal_idx: 0 }" # Load 2 + - "ArrayPush { arr: 0, value: 1 }" # Push 2 to array + - "LoadNull { dest: 2 }" # Load null value + - "ArrayPush { arr: 0, value: 2 }" # Push null to array + - "Load { dest: 3, literal_idx: 1 }" # Load 4 + - "ArrayPush { arr: 0, value: 3 }" # Push 4 to array + - "LoopStart { params_index: 0 }" # Start universal loop + - "LoadNull { dest: 7 }" # Load null for comparison + - "Ne { dest: 8, left: 5, right: 7 }" # Check if current value != null + - "AssertCondition { condition: 8 }" # Assert the condition result for universal logic + - "LoopNext { body_start: 8, loop_end: 12 }" # Continue to next iteration + - "Return { value: 6 }" # Return result + want_result: false diff --git a/tests/rvm/vm/suites/null_undefined_handling.yaml b/tests/rvm/vm/suites/null_undefined_handling.yaml new file mode 100644 index 0000000..393b90c --- /dev/null +++ b/tests/rvm/vm/suites/null_undefined_handling.yaml @@ -0,0 +1,376 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Null and Undefined Handling Test Suite +# Tests null and undefined behavior across all instruction families +# Covers arithmetic, comparisons, logical ops, indexing, loops, and comprehensions + +cases: + - note: load_null_basic + description: LoadNull loads null value + example_rego: "null" + literals: [] + instructions: + - "LoadNull { dest: 0 }" + - "Return { value: 0 }" + want_result: null + + - note: null_in_arithmetic_add + description: Adding null is a type error + example_rego: "null + 1" + literals: + - 1 + instructions: + - "LoadNull { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Add { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_error: "Cannot add Null" + + - note: null_in_arithmetic_sub + description: Subtracting null is a type error + example_rego: "5 - null" + literals: + - 5 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "LoadNull { dest: 1 }" + - "Sub { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_error: "Cannot subtract Number(5)" + + - note: null_in_arithmetic_mul + description: Multiplying null is a type error + example_rego: "null * 3" + literals: + - 3 + instructions: + - "LoadNull { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Mul { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_error: "Cannot multiply Null" + + - note: null_in_comparison_eq + description: null equals null + example_rego: "null == null" + literals: [] + instructions: + - "LoadNull { dest: 0 }" + - "LoadNull { dest: 1 }" + - "Eq { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: true + + - note: null_not_equal_to_number + description: null is not equal to numbers + example_rego: "null == 0" + literals: + - 0 + instructions: + - "LoadNull { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Eq { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: false + + - note: null_not_equal_to_false + description: null is not equal to false + example_rego: "null == false" + literals: [] + instructions: + - "LoadNull { dest: 0 }" + - "LoadFalse { dest: 1 }" + - "Eq { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: false + + - note: null_in_comparison_lt + description: Ordering comparison treats null as less than numbers + example_rego: "null < 5" + literals: + - 5 + instructions: + - "LoadNull { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Lt { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: true + + - note: null_in_logical_and + description: Logical AND treats null as truthy + example_rego: "null && true" + literals: [] + instructions: + - "LoadNull { dest: 0 }" + - "LoadTrue { dest: 1 }" + - "And { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: true + + - note: null_in_logical_or + description: Logical OR treats null as truthy + example_rego: "null || false" + literals: [] + instructions: + - "LoadNull { dest: 0 }" + - "LoadFalse { dest: 1 }" + - "Or { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: true + + - note: null_in_logical_not + description: Logical NOT treats null as truthy (returns false) + example_rego: "not null" + literals: [] + instructions: + - "LoadNull { dest: 0 }" + - "Not { dest: 1, operand: 0 }" + - "Return { value: 1 }" + want_result: false + + - note: null_as_array_index + description: Indexing array with null key + example_rego: "[1, 2, 3][null]" + literals: + - [1, 2, 3] + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "LoadNull { dest: 1 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" # null is not a valid array index + + - note: null_as_object_key + description: Indexing object with null key + example_rego: "{\"a\": 1, \"b\": 2}[null]" + literals: + - {"a": 1, "b": 2} + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "LoadNull { dest: 1 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" # null key doesn't exist + + - note: null_in_contains_check + description: Contains check with null + example_rego: "null in [1, 2, null, 3]" + literals: + - [1, 2, null, 3] + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "LoadNull { dest: 1 }" + - "Contains { dest: 2, collection: 0, value: 1 }" + - "Return { value: 2 }" + want_result: true + + - note: null_in_set + description: null can be a set member + example_rego: "{1, null, 3}" + literals: + - 1 + - 3 + instructions: + - "SetNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "SetAdd { set: 0, value: 1 }" + - "LoadNull { dest: 2 }" + - "SetAdd { set: 0, value: 2 }" + - "Load { dest: 3, literal_idx: 1 }" + - "SetAdd { set: 0, value: 3 }" + - "Return { value: 0 }" + want_result: + set!: + - 1 + - null + - 3 + + - note: null_in_array + description: null can be an array element + example_rego: "[1, null, 3]" + literals: + - 1 + - 3 + instruction_params: + array_create_params: + - dest: 0 + elements: [1, 2, 3] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "LoadNull { dest: 2 }" + - "Load { dest: 3, literal_idx: 1 }" + - "ArrayCreate { params_index: 0 }" + - "Return { value: 0 }" + want_result: [1, null, 3] + + - note: null_in_object_value + description: null can be an object value + example_rego: "{\"a\": 1, \"b\": null}" + literals: + - "a" + - 1 + - "b" + - {} + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 3 + literal_key_fields: [] + fields: + - [1, 2] + - [3, 4] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Load { dest: 3, literal_idx: 2 }" + - "LoadNull { dest: 4 }" + - "ObjectCreate { params_index: 0 }" + - "Return { value: 0 }" + want_result: {"a": 1, "b": null} + + - note: undefined_in_arithmetic + description: Arithmetic with undefined register produces undefined + example_rego: "undefined_var + 1" + literals: + - 1 + instructions: + # r0 is undefined (not loaded) + - "Load { dest: 1, literal_idx: 0 }" + - "Add { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" + + - note: undefined_in_comparison + description: Comparison with undefined returns undefined + example_rego: "undefined_var == 5" + literals: + - 5 + instructions: + # r0 is undefined + - "Load { dest: 1, literal_idx: 0 }" + - "Eq { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" + + - note: undefined_in_logical_ops + description: Logical operations with undefined return undefined + example_rego: "undefined_var && true" + literals: [] + instructions: + # r0 is undefined + - "LoadTrue { dest: 1 }" + - "And { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" + + - note: undefined_indexing + description: Indexing undefined returns undefined + example_rego: "undefined_var[\"key\"]" + literals: + - "key" + instructions: + # r0 is undefined + - "Load { dest: 1, literal_idx: 0 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" + + - note: undefined_in_array_create + description: ArrayCreate with undefined element returns undefined + literals: + - 1 + - 2 + instruction_params: + array_create_params: + - dest: 0 + elements: [1, 2, 3] # r3 is undefined + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + # r3 is undefined + - "ArrayCreate { params_index: 0 }" + - "Return { value: 0 }" + want_result: "#undefined" + + - note: undefined_in_object_create_key + description: ObjectCreate with undefined key returns undefined + literals: + - 1 + - 2 + - {} + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 2 + literal_key_fields: [] + fields: + - [1, 2] + - [3, 4] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + # r3, r4 are undefined + - "ObjectCreate { params_index: 0 }" + - "Return { value: 0 }" + want_result: "#undefined" + + - note: undefined_in_object_create_value + description: ObjectCreate with undefined value returns undefined + literals: + - "key1" + - "key2" + - 1 + - {} + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 3 + literal_key_fields: [] + fields: + - [1, 3] + - [2, 4] # r4 is undefined + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Load { dest: 3, literal_idx: 2 }" + # r4 is undefined + - "ObjectCreate { params_index: 0 }" + - "Return { value: 0 }" + want_result: "#undefined" + + - note: undefined_in_loop_collection + description: Loop over undefined collection fails gracefully + literals: [] + instruction_params: + loop_params: + - mode: "ForEach" + collection: 0 # r0 is undefined + key_reg: 1 + value_reg: 2 + result_reg: 4 + body_start: 1 + loop_end: 3 + instructions: + # r0 is undefined + - "LoopStart { params_index: 0 }" + # Loop body (never executed because collection is undefined) + - "LoadTrue { dest: 3 }" + - "LoopNext { body_start: 1, loop_end: 3 }" + - "Return { value: 3 }" + want_result: "#undefined" # Loop over undefined collection yields undefined + + - note: undefined_propagation_through_chain + description: Undefined propagates through operation chain + example_rego: "(undefined_var + 1) * 2" + literals: + - 1 + - 2 + instructions: + # r0 is undefined + - "Load { dest: 1, literal_idx: 0 }" + - "Add { dest: 2, left: 0, right: 1 }" # r2 becomes undefined + - "Load { dest: 3, literal_idx: 1 }" + - "Mul { dest: 4, left: 2, right: 3 }" # r4 becomes undefined + - "Return { value: 4 }" + want_result: "#undefined" \ No newline at end of file diff --git a/tests/rvm/vm/suites/object_operations.yaml b/tests/rvm/vm/suites/object_operations.yaml new file mode 100644 index 0000000..6cc96df --- /dev/null +++ b/tests/rvm/vm/suites/object_operations.yaml @@ -0,0 +1,276 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Object Operations Test Suite +# Tests advanced object creation, manipulation, and edge cases +# Covers dynamic keys, collisions, non-string keys, and template validation + +cases: + - note: object_key_collision_overwrite + description: Setting same key twice should overwrite the value + example_rego: "{\"key\": 1, \"key\": 2}" + literals: + - {} + - "key" + - 1 + - 2 + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + instructions: + - "ObjectCreate { params_index: 0 }" + - "Load { dest: 1, literal_idx: 1 }" # key + - "Load { dest: 2, literal_idx: 2 }" # value 1 + - "ObjectSet { obj: 0, key: 1, value: 2 }" + - "Load { dest: 3, literal_idx: 3 }" # value 2 + - "ObjectSet { obj: 0, key: 1, value: 3 }" # Overwrite + - "Return { value: 0 }" + want_result: {"key": 2} + + - note: object_dynamic_key_generation + description: Generate object keys dynamically from loop iteration + example_rego: "{sprintf(\"key_%d\", [i]): i | i := [1, 2][_]}" + literals: + - {} + - 1 + - 2 + - "key_1" + - "key_2" + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + loop_params: + - mode: "ForEach" + collection: 5 + key_reg: 10 + value_reg: 11 + result_reg: 12 + body_start: 7 + loop_end: 13 + instructions: + - "ObjectCreate { params_index: 0 }" + - "ArrayNew { dest: 5 }" + - "Load { dest: 1, literal_idx: 1 }" + - "ArrayPush { arr: 5, value: 1 }" + - "Load { dest: 2, literal_idx: 2 }" + - "ArrayPush { arr: 5, value: 2 }" + - "LoopStart { params_index: 0 }" + # Generate dynamic key based on value + - "Load { dest: 20, literal_idx: 3 }" # "key_1" when i=1 + - "Load { dest: 21, literal_idx: 4 }" # "key_2" when i=2 + # For simplicity, we'll set both keys + - "ObjectSet { obj: 0, key: 20, value: 1 }" + - "Load { dest: 3, literal_idx: 2 }" + - "ObjectSet { obj: 0, key: 21, value: 3 }" + - "LoopNext { body_start: 7, loop_end: 13 }" + - "Return { value: 0 }" + want_result: {"key_1": 1, "key_2": 2} + + - note: object_number_keys + description: Object with number keys + example_rego: "{1: \"one\", 2: \"two\", 42: \"answer\"}" + literals: + - {} + - 1 + - "one" + - 2 + - "two" + - 42 + - "answer" + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + instructions: + - "ObjectCreate { params_index: 0 }" + - "Load { dest: 1, literal_idx: 1 }" # key 1 + - "Load { dest: 2, literal_idx: 2 }" # value "one" + - "ObjectSet { obj: 0, key: 1, value: 2 }" + - "Load { dest: 3, literal_idx: 3 }" # key 2 + - "Load { dest: 4, literal_idx: 4 }" # value "two" + - "ObjectSet { obj: 0, key: 3, value: 4 }" + - "Load { dest: 5, literal_idx: 5 }" # key 42 + - "Load { dest: 6, literal_idx: 6 }" # value "answer" + - "ObjectSet { obj: 0, key: 5, value: 6 }" + - "Return { value: 0 }" + want_result: {1: "one", 2: "two", 42: "answer"} + + - note: object_boolean_keys + description: Object with boolean keys + example_rego: "{true: \"yes\", false: \"no\"}" + literals: + - {} + - "yes" + - "no" + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + instructions: + - "ObjectCreate { params_index: 0 }" + - "LoadTrue { dest: 1 }" + - "Load { dest: 2, literal_idx: 1 }" # value "yes" + - "ObjectSet { obj: 0, key: 1, value: 2 }" + - "LoadFalse { dest: 3 }" + - "Load { dest: 4, literal_idx: 2 }" # value "no" + - "ObjectSet { obj: 0, key: 3, value: 4 }" + - "Return { value: 0 }" + want_result: {true: "yes", false: "no"} + + - note: object_null_key + description: Object with null key + example_rego: "{null: \"nothing\"}" + literals: + - {} + - "nothing" + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + instructions: + - "ObjectCreate { params_index: 0 }" + - "LoadNull { dest: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "ObjectSet { obj: 0, key: 1, value: 2 }" + - "Return { value: 0 }" + want_result: {null: "nothing"} + + - note: object_empty_string_key + description: Object with empty string key + example_rego: "{\"\": \"empty_key\"}" + literals: + - {} + - "" + - "empty_key" + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + instructions: + - "ObjectCreate { params_index: 0 }" + - "Load { dest: 1, literal_idx: 1 }" # empty string key + - "Load { dest: 2, literal_idx: 2 }" # value + - "ObjectSet { obj: 0, key: 1, value: 2 }" + - "Return { value: 0 }" + want_result: {"": "empty_key"} + + - note: object_set_undefined_value + description: Setting undefined value in object keeps it + literals: + - {} + - "key" + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + instructions: + - "ObjectCreate { params_index: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + # r2 is undefined (never loaded) + - "ObjectSet { obj: 0, key: 1, value: 2 }" + - "Return { value: 0 }" + want_result: {"key": "#undefined"} + + - note: object_create_with_template + description: ObjectCreate with pre-populated template + literals: + - {"existing": "value", "num": 42} + - "new_key" + - "new_value" + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + instructions: + - "ObjectCreate { params_index: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Load { dest: 2, literal_idx: 2 }" + - "ObjectSet { obj: 0, key: 1, value: 2 }" + - "Return { value: 0 }" + want_result: {"existing": "value", "num": 42, "new_key": "new_value"} + + - note: object_nested_structure + description: Create deeply nested object structure + example_rego: "{\"outer\": {\"middle\": {\"inner\": \"value\"}}}" + literals: + - {} + - "outer" + - "middle" + - "inner" + - "value" + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + instructions: + - "ObjectCreate { params_index: 0 }" # outer + - "Load { dest: 10, literal_idx: 0 }" # middle object + - "Load { dest: 11, literal_idx: 0 }" # inner object + - "Load { dest: 12, literal_idx: 3 }" # "inner" key + - "Load { dest: 13, literal_idx: 4 }" # "value" + - "ObjectSet { obj: 11, key: 12, value: 13 }" # {inner: "value"} + - "Load { dest: 14, literal_idx: 2 }" # "middle" key + - "ObjectSet { obj: 10, key: 14, value: 11 }" # {middle: {...}} + - "Load { dest: 15, literal_idx: 1 }" # "outer" key + - "ObjectSet { obj: 0, key: 15, value: 10 }" # {outer: {...}} + - "Return { value: 0 }" + want_result: {"outer": {"middle": {"inner": "value"}}} + + - note: object_mixed_value_types + description: Object with values of different types + example_rego: "{\"str\": \"text\", \"num\": 42, \"bool\": true, \"null\": null, \"arr\": [1, 2]}" + literals: + - {} + - "str" + - "text" + - "num" + - 42 + - "bool" + - "null" + - "arr" + - [1, 2] + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 0 + literal_key_fields: [] + fields: [] + instructions: + - "ObjectCreate { params_index: 0 }" + - "Load { dest: 1, literal_idx: 1 }" # "str" + - "Load { dest: 2, literal_idx: 2 }" # "text" + - "ObjectSet { obj: 0, key: 1, value: 2 }" + - "Load { dest: 3, literal_idx: 3 }" # "num" + - "Load { dest: 4, literal_idx: 4 }" # 42 + - "ObjectSet { obj: 0, key: 3, value: 4 }" + - "Load { dest: 5, literal_idx: 5 }" # "bool" + - "LoadTrue { dest: 6 }" + - "ObjectSet { obj: 0, key: 5, value: 6 }" + - "Load { dest: 7, literal_idx: 6 }" # "null" + - "LoadNull { dest: 8 }" + - "ObjectSet { obj: 0, key: 7, value: 8 }" + - "Load { dest: 9, literal_idx: 7 }" # "arr" + - "Load { dest: 10, literal_idx: 8 }" # [1, 2] + - "ObjectSet { obj: 0, key: 9, value: 10 }" + - "Return { value: 0 }" + want_result: {"str": "text", "num": 42, "bool": true, "null": null, "arr": [1, 2]} diff --git a/tests/rvm/vm/suites/predefined.yaml b/tests/rvm/vm/suites/predefined.yaml new file mode 100644 index 0000000..f3d324d --- /dev/null +++ b/tests/rvm/vm/suites/predefined.yaml @@ -0,0 +1,165 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Predefined Global Bindings Test Suite +# Tests Rego's predefined data and input global bindings +# These bindings are always available in Rego policies + +cases: + - note: load_data_basic + description: Test loading global data object + data: + users: ["alice", "bob"] + config: + debug: true + timeout: 30 + input: null + literals: ["users"] + instructions: + - "LoadData { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" # Load "users" literal + - "Index { dest: 2, container: 0, key: 1 }" # data.users + - "Return { value: 2 }" + want_result: ["alice", "bob"] + + - note: load_input_basic + description: Test loading global input object + data: null + input: + request: + method: "GET" + path: "/api/users" + user: + id: 123 + role: "admin" + literals: ["request", "method"] + instructions: + - "LoadInput { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" # Load "request" literal + - "Index { dest: 2, container: 0, key: 1 }" # input.request + - "Load { dest: 3, literal_idx: 1 }" # Load "method" literal + - "Index { dest: 4, container: 2, key: 3 }" # input.request.method + - "Return { value: 4 }" + want_result: "GET" + + - note: data_and_input_combined + description: Test using both data and input in same expression + data: + permissions: + admin: ["read", "write", "delete"] + user: ["read"] + input: + user: + role: "admin" + literals: ["permissions", "user", "role"] + instructions: + - "LoadData { dest: 0 }" # Load data + - "LoadInput { dest: 1 }" # Load input + - "Load { dest: 2, literal_idx: 1 }" # Load "user" literal + - "Index { dest: 3, container: 1, key: 2 }" # input.user + - "Load { dest: 4, literal_idx: 2 }" # Load "role" literal + - "Index { dest: 5, container: 3, key: 4 }" # input.user.role + - "Load { dest: 6, literal_idx: 0 }" # Load "permissions" literal + - "Index { dest: 7, container: 0, key: 6 }" # data.permissions + - "Index { dest: 8, container: 7, key: 5 }" # data.permissions[input.user.role] + - "Return { value: 8 }" + want_result: ["read", "write", "delete"] + + - note: data_null_handling + description: Test behavior when data is null + data: null + input: + test: "value" + literals: [] + instructions: + - "LoadData { dest: 0 }" + - "Return { value: 0 }" + want_result: null + + - note: input_null_handling + description: Test behavior when input is null + data: + test: "value" + input: null + literals: [] + instructions: + - "LoadInput { dest: 0 }" + - "Return { value: 0 }" + want_result: null + + - note: nested_data_access + description: Test deep nested data access + data: + api: + v1: + endpoints: + users: "/api/v1/users" + posts: "/api/v1/posts" + input: null + literals: ["api", "v1", "endpoints", "users"] + instructions: + - "LoadData { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" # "api" + - "Index { dest: 2, container: 0, key: 1 }" # data.api + - "Load { dest: 3, literal_idx: 1 }" # "v1" + - "Index { dest: 4, container: 2, key: 3 }" # data.api.v1 + - "Load { dest: 5, literal_idx: 2 }" # "endpoints" + - "Index { dest: 6, container: 4, key: 5 }" # data.api.v1.endpoints + - "Load { dest: 7, literal_idx: 3 }" # "users" + - "Index { dest: 8, container: 6, key: 7 }" # data.api.v1.endpoints.users + - "Return { value: 8 }" + want_result: "/api/v1/users" + + - note: array_access_with_input + description: Test array indexing with input values + data: + colors: ["red", "green", "blue"] + input: + selected_index: 1 + literals: ["colors", "selected_index"] + instructions: + - "LoadData { dest: 0 }" + - "LoadInput { dest: 1 }" + - "Load { dest: 2, literal_idx: 0 }" # "colors" + - "Index { dest: 3, container: 0, key: 2 }" # data.colors + - "Load { dest: 4, literal_idx: 1 }" # "selected_index" + - "Index { dest: 5, container: 1, key: 4 }" # input.selected_index + - "Index { dest: 6, container: 3, key: 5 }" # data.colors[input.selected_index] + - "Return { value: 6 }" + want_result: "green" + + - note: data_only_access + description: Test accessing data when input is not needed + data: + settings: + theme: "dark" + notifications: true + input: null + literals: ["settings", "theme"] + instructions: + - "LoadData { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" # "settings" + - "Index { dest: 2, container: 0, key: 1 }" # data.settings + - "Load { dest: 3, literal_idx: 1 }" # "theme" + - "Index { dest: 4, container: 2, key: 3 }" # data.settings.theme + - "Return { value: 4 }" + want_result: "dark" + + - note: input_only_access + description: Test accessing input when data is not needed + data: null + input: + request: + headers: + authorization: "Bearer token123" + literals: ["request", "headers", "authorization"] + instructions: + - "LoadInput { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" # "request" + - "Index { dest: 2, container: 0, key: 1 }" # input.request + - "Load { dest: 3, literal_idx: 1 }" # "headers" + - "Index { dest: 4, container: 2, key: 3 }" # input.request.headers + - "Load { dest: 5, literal_idx: 2 }" # "authorization" + - "Index { dest: 6, container: 4, key: 5 }" # input.request.headers.authorization + - "Return { value: 6 }" + want_result: "Bearer token123" diff --git a/tests/rvm/vm/suites/resource_limits.yaml b/tests/rvm/vm/suites/resource_limits.yaml new file mode 100644 index 0000000..07d60b5 --- /dev/null +++ b/tests/rvm/vm/suites/resource_limits.yaml @@ -0,0 +1,217 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Resource Limits Test Suite +# Tests instruction count limits, recursion depth, and resource exhaustion scenarios +# Verifies VM handles resource constraints gracefully + +cases: + - note: instruction_limit_in_simple_loop + description: Instruction limit exceeded in simple loop + example_rego: "some x in [1, 2, 3, 4, 5]; x > 0" + literals: + - 1 + - 2 + - 3 + - 4 + - 5 + - 0 + max_instructions: 10 # Limit low enough that execution exceeds it before completion + instruction_params: + loop_params: + - mode: "Any" + collection: 0 + key_reg: 10 + value_reg: 11 + result_reg: 12 + body_start: 13 + loop_end: 17 + instructions: + - "ArrayNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "ArrayPush { arr: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "ArrayPush { arr: 0, value: 2 }" + - "Load { dest: 3, literal_idx: 2 }" + - "ArrayPush { arr: 0, value: 3 }" + - "Load { dest: 4, literal_idx: 3 }" + - "ArrayPush { arr: 0, value: 4 }" + - "Load { dest: 5, literal_idx: 4 }" + - "ArrayPush { arr: 0, value: 5 }" + - "LoopStart { params_index: 0 }" + - "Load { dest: 13, literal_idx: 5 }" + - "Gt { dest: 14, left: 11, right: 13 }" + - "AssertCondition { condition: 14 }" + - "LoopNext { body_start: 13, loop_end: 17 }" + - "Return { value: 12 }" + want_error: "exceeded maximum instruction limit" + + - note: instruction_limit_in_comprehension + description: Instruction limit exceeded during comprehension + example_rego: "[x | x := [1, 2, 3, 4, 5][_]]" + literals: + - 1 + - 2 + - 3 + - 4 + - 5 + max_instructions: 25 + instruction_params: + comprehension_begin_params: + - mode: "Array" + collection_reg: 0 + result_reg: 0 + key_reg: 10 + value_reg: 11 + body_start: 13 + comprehension_end: 17 + loop_params: + - mode: "ForEach" + collection: 6 + key_reg: 10 + value_reg: 11 + result_reg: 12 + body_start: 13 + loop_end: 17 + instructions: + - "ArrayNew { dest: 6 }" + - "Load { dest: 1, literal_idx: 0 }" + - "ArrayPush { arr: 6, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "ArrayPush { arr: 6, value: 2 }" + - "Load { dest: 3, literal_idx: 2 }" + - "ArrayPush { arr: 6, value: 3 }" + - "Load { dest: 4, literal_idx: 3 }" + - "ArrayPush { arr: 6, value: 4 }" + - "Load { dest: 5, literal_idx: 4 }" + - "ArrayPush { arr: 6, value: 5 }" + - "ComprehensionBegin { params_index: 0 }" + - "LoopStart { params_index: 0 }" + - "ComprehensionYield { value_reg: 11 }" + - "LoopNext { body_start: 13, loop_end: 17 }" + - "ComprehensionEnd" + - "Return { value: 0 }" + want_error: "exceeded maximum instruction limit" + + - note: instruction_limit_in_nested_loops + description: Instruction limit exceeded in nested loops + example_rego: "some x in [1, 2]; some y in [3, 4]; x + y > 0" + literals: + - 1 + - 2 + - 3 + - 4 + - 0 + max_instructions: 30 + instruction_params: + loop_params: + - mode: "Any" + collection: 0 + key_reg: 10 + value_reg: 11 + result_reg: 12 + body_start: 5 + loop_end: 18 + - mode: "Any" + collection: 2 + key_reg: 20 + value_reg: 21 + result_reg: 22 + body_start: 8 + loop_end: 16 + instructions: + - "ArrayNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "ArrayPush { arr: 0, value: 1 }" + - "Load { dest: 3, literal_idx: 1 }" + - "ArrayPush { arr: 0, value: 3 }" + - "LoopStart { params_index: 0 }" + - "ArrayNew { dest: 2 }" + - "Load { dest: 4, literal_idx: 2 }" + - "ArrayPush { arr: 2, value: 4 }" + - "Load { dest: 5, literal_idx: 3 }" + - "ArrayPush { arr: 2, value: 5 }" + - "LoopStart { params_index: 1 }" + - "Add { dest: 30, left: 11, right: 21 }" + - "Load { dest: 31, literal_idx: 4 }" + - "Gt { dest: 32, left: 30, right: 31 }" + - "AssertCondition { condition: 32 }" + - "LoopNext { body_start: 8, loop_end: 16 }" + - "AssertCondition { condition: 22 }" + - "LoopNext { body_start: 5, loop_end: 18 }" + - "Return { value: 12 }" + want_error: "exceeded maximum instruction limit" + + - note: large_literal_table_access + description: Access literal near u16 bounds (valid case) + literals: + - 42 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Return { value: 0 }" + want_result: 42 + + - note: literal_index_out_of_bounds + description: Literal index beyond table bounds should error + literals: + - 42 + instructions: + - "Load { dest: 0, literal_idx: 999 }" + - "Return { value: 0 }" + want_error: "Literal index 999 out of bounds" + + - note: instruction_limit_with_early_return + description: Instruction limit allows early successful completion + example_rego: "some x in [1, 2]; x == 1" + literals: + - 1 + - 2 + max_instructions: 200 # Allow enough headroom for both execution modes + instruction_params: + loop_params: + - mode: "Any" + collection: 0 + key_reg: 10 + value_reg: 11 + result_reg: 12 + body_start: 6 + loop_end: 9 + instructions: + - "ArrayNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "ArrayPush { arr: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "ArrayPush { arr: 0, value: 2 }" + - "LoopStart { params_index: 0 }" + - "Load { dest: 13, literal_idx: 0 }" + - "Eq { dest: 14, left: 11, right: 13 }" + - "AssertCondition { condition: 14 }" + - "LoopNext { body_start: 6, loop_end: 9 }" + - "Return { value: 12 }" + want_result: true + + - note: many_registers_usage + description: Test using high register numbers near upper bound without error + example_rego: "Store values in registers near VM upper bound" + literals: + - 1 + - 2 + - 3 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 25, literal_idx: 1 }" + - "Load { dest: 49, literal_idx: 2 }" + - "Add { dest: 30, left: 0, right: 25 }" + - "Add { dest: 48, left: 30, right: 49 }" + - "Return { value: 48 }" + want_result: 6 + + - note: zero_instruction_limit + description: Zero instruction limit should error immediately + literals: + - 42 + max_instructions: 0 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Return { value: 0 }" + want_error: "exceeded maximum instruction limit" diff --git a/tests/rvm/vm/suites/serialization.yaml b/tests/rvm/vm/suites/serialization.yaml new file mode 100644 index 0000000..474d785 --- /dev/null +++ b/tests/rvm/vm/suites/serialization.yaml @@ -0,0 +1,423 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Serialization Test Suite +# Tests round-trip serialization of compiled RVM programs +# Covers all instruction types, large programs, and edge cases + +cases: + - note: serialization_basic_arithmetic + description: Serialize and deserialize simple arithmetic program + example_rego: "5 + 3" + literals: + - 5 + - 3 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Add { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: 8 + + - note: serialization_comparisons + description: Serialize comparison instructions + example_rego: "10 > 5 && 3 < 7" + literals: + - 10 + - 5 + - 3 + - 7 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Gt { dest: 2, left: 0, right: 1 }" + - "Load { dest: 3, literal_idx: 2 }" + - "Load { dest: 4, literal_idx: 3 }" + - "Lt { dest: 5, left: 3, right: 4 }" + - "And { dest: 6, left: 2, right: 5 }" + - "Return { value: 6 }" + want_result: true + + - note: serialization_array_create + description: Serialize ArrayCreate with instruction_params + example_rego: "[1, 2, 3]" + literals: + - 1 + - 2 + - 3 + instruction_params: + array_create_params: + - dest: 0 + elements: [1, 2, 3] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Load { dest: 3, literal_idx: 2 }" + - "ArrayCreate { params_index: 0 }" + - "Return { value: 0 }" + want_result: [1, 2, 3] + + - note: serialization_object_create + description: Serialize ObjectCreate with complex params + example_rego: "{\"a\": 1, \"b\": 2}" + literals: + - "a" + - 1 + - "b" + - 2 + - {} + instruction_params: + object_create_params: + - dest: 0 + template_literal_idx: 4 + literal_key_fields: [] + fields: + - [1, 2] + - [3, 4] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Load { dest: 3, literal_idx: 2 }" + - "Load { dest: 4, literal_idx: 3 }" + - "ObjectCreate { params_index: 0 }" + - "Return { value: 0 }" + want_result: {"a": 1, "b": 2} + + - note: serialization_set_operations + description: Serialize set creation and operations + example_rego: "{1, 2, 3}" + literals: + - 1 + - 2 + - 3 + instructions: + - "SetNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "SetAdd { set: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "SetAdd { set: 0, value: 2 }" + - "Load { dest: 3, literal_idx: 2 }" + - "SetAdd { set: 0, value: 3 }" + - "Return { value: 0 }" + want_result: + set!: + - 1 + - 2 + - 3 + + - note: serialization_loop_foreach + description: Serialize ForEach loop with params + example_rego: "[x | x = [1, 2, 3][_]]" + literals: + - 1 + - 2 + - 3 + instruction_params: + array_create_params: + - dest: 0 + elements: [1, 2, 3] + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 9 + body_start: 6 + loop_end: 8 + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Load { dest: 3, literal_idx: 2 }" + - "ArrayCreate { params_index: 0 }" + - "ArrayNew { dest: 6 }" + - "LoopStart { params_index: 0 }" + - "ArrayPush { arr: 6, value: 5 }" + - "LoopNext { body_start: 6, loop_end: 8 }" + - "Return { value: 6 }" + want_result: [1, 2, 3] + + - note: serialization_loop_any + description: Serialize Any loop + example_rego: "some x in [1, 2, 3]; x > 2" + literals: + - 1 + - 2 + - 3 + instruction_params: + array_create_params: + - dest: 0 + elements: [1, 2, 3] + loop_params: + - mode: "Existential" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 6 + body_start: 5 + loop_end: 8 + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Load { dest: 3, literal_idx: 2 }" + - "ArrayCreate { params_index: 0 }" + - "LoopStart { params_index: 0 }" + - "Gt { dest: 8, left: 5, right: 2 }" + - "AssertCondition { condition: 8 }" + - "LoopNext { body_start: 5, loop_end: 8 }" + - "Return { value: 6 }" + want_result: true + + - note: serialization_comprehension_array + description: Serialize array comprehension + example_rego: "[x * 2 | x = [1, 2, 3][_]]" + literals: + - 1 + - 2 + - 3 + instruction_params: + array_create_params: + - dest: 0 + elements: [1, 2, 3] + loop_params: + - mode: "ForEach" + collection: 0 + key_reg: 4 + value_reg: 5 + result_reg: 9 + body_start: 6 + loop_end: 9 + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Load { dest: 3, literal_idx: 2 }" + - "ArrayCreate { params_index: 0 }" + - "ArrayNew { dest: 7 }" + - "LoopStart { params_index: 0 }" + - "Mul { dest: 6, left: 5, right: 2 }" + - "ArrayPush { arr: 7, value: 6 }" + - "LoopNext { body_start: 6, loop_end: 9 }" + - "Return { value: 7 }" + want_result: [2, 4, 6] + + - note: serialization_indexed_access + description: Serialize indexing instructions + example_rego: "data.users[0].name" + literals: + - {"users": [{"name": "Alice"}, {"name": "Bob"}]} + - "users" + - 0 + - "name" + instruction_params: + chained_index_params: + - dest: 4 + root: 0 + path_components: + - literal_idx: 1 + - literal_idx: 2 + - literal_idx: 3 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Load { dest: 2, literal_idx: 2 }" + - "Load { dest: 3, literal_idx: 3 }" + - "ChainedIndex { params_index: 0 }" + - "Return { value: 4 }" + want_result: "Alice" + + - note: serialization_conditional_branching + description: Serialize conditional instructions + example_rego: "if true then 1 else 2" + literals: + - 1 + - 2 + instructions: + - "LoadTrue { dest: 0 }" + - "AssertCondition { condition: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Return { value: 1 }" + want_result: 1 + + - note: serialization_null_and_undefined + description: Serialize null and undefined handling + example_rego: "null" + literals: [] + instructions: + - "LoadNull { dest: 0 }" + - "Return { value: 0 }" + want_result: null + + - note: serialization_mixed_types + description: Serialize program with all value types + example_rego: "[1, \"text\", true, false, null, [2, 3], {\"k\": \"v\"}]" + literals: + - 1 + - "text" + - 2 + - 3 + - "k" + - "v" + - {} + instruction_params: + array_create_params: + - dest: 6 + elements: [3, 4] + - dest: 0 + elements: [1, 2, 7, 8, 9, 6, 10] + object_create_params: + - dest: 10 + template_literal_idx: 6 + literal_key_fields: [] + fields: + - [5, 11] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Load { dest: 3, literal_idx: 2 }" + - "Load { dest: 4, literal_idx: 3 }" + - "ArrayCreate { params_index: 0 }" + - "LoadTrue { dest: 7 }" + - "LoadFalse { dest: 8 }" + - "LoadNull { dest: 9 }" + - "Load { dest: 5, literal_idx: 4 }" + - "Load { dest: 11, literal_idx: 5 }" + - "ObjectCreate { params_index: 0 }" + - "ArrayCreate { params_index: 1 }" + - "Return { value: 0 }" + want_result: [1, "text", true, false, null, [2, 3], {"k": "v"}] + + - note: serialization_large_program_50_instructions + description: Large program with many instructions (stress test) + example_rego: "Complex computation chain" + literals: + - 1 + - 2 + - 3 + - 4 + - 5 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Add { dest: 2, left: 0, right: 1 }" + - "Load { dest: 3, literal_idx: 2 }" + - "Mul { dest: 4, left: 2, right: 3 }" + - "Load { dest: 5, literal_idx: 3 }" + - "Sub { dest: 6, left: 4, right: 5 }" + - "Load { dest: 7, literal_idx: 4 }" + - "Div { dest: 8, left: 6, right: 7 }" + - "Load { dest: 9, literal_idx: 0 }" + - "Add { dest: 10, left: 8, right: 9 }" + - "Load { dest: 11, literal_idx: 1 }" + - "Mul { dest: 12, left: 10, right: 11 }" + - "Load { dest: 13, literal_idx: 2 }" + - "Add { dest: 14, left: 12, right: 13 }" + - "Load { dest: 15, literal_idx: 3 }" + - "Sub { dest: 16, left: 14, right: 15 }" + - "Load { dest: 17, literal_idx: 4 }" + - "Mul { dest: 18, left: 16, right: 17 }" + - "Load { dest: 19, literal_idx: 0 }" + - "Div { dest: 20, left: 18, right: 19 }" + - "Load { dest: 21, literal_idx: 1 }" + - "Add { dest: 22, left: 20, right: 21 }" + - "Load { dest: 23, literal_idx: 2 }" + - "Mul { dest: 24, left: 22, right: 23 }" + - "Load { dest: 25, literal_idx: 3 }" + - "Sub { dest: 26, left: 24, right: 25 }" + - "Load { dest: 27, literal_idx: 4 }" + - "Add { dest: 28, left: 26, right: 27 }" + - "Load { dest: 29, literal_idx: 0 }" + - "Mul { dest: 30, left: 28, right: 29 }" + - "Load { dest: 31, literal_idx: 1 }" + - "Div { dest: 32, left: 30, right: 31 }" + - "Load { dest: 33, literal_idx: 2 }" + - "Add { dest: 34, left: 32, right: 33 }" + - "Load { dest: 35, literal_idx: 3 }" + - "Sub { dest: 36, left: 34, right: 35 }" + - "Load { dest: 37, literal_idx: 4 }" + - "Mul { dest: 38, left: 36, right: 37 }" + - "Load { dest: 39, literal_idx: 0 }" + - "Add { dest: 40, left: 38, right: 39 }" + - "Load { dest: 41, literal_idx: 1 }" + - "Sub { dest: 42, left: 40, right: 41 }" + - "Load { dest: 43, literal_idx: 2 }" + - "Mul { dest: 44, left: 42, right: 43 }" + - "Load { dest: 45, literal_idx: 3 }" + - "Div { dest: 46, left: 44, right: 45 }" + - "Load { dest: 47, literal_idx: 4 }" + - "Add { dest: 48, left: 46, right: 47 }" + - "Return { value: 48 }" + want_result: 98 + + - note: serialization_large_literal_table + description: Program with many literal values + literals: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - "a" + - "b" + - "c" + - "d" + - "e" + - [1, 2, 3] + - {"key": "value"} + - true + - false + - null + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 5 }" + - "Add { dest: 2, left: 0, right: 1 }" + - "Load { dest: 3, literal_idx: 10 }" + - "Load { dest: 4, literal_idx: 15 }" + - "Return { value: 2 }" + want_result: 7 + + - note: serialization_nested_structures + description: Serialize deeply nested data structures + example_rego: "[{\"a\": [1, {\"b\": [2, 3]}]}]" + literals: + - 1 + - 2 + - 3 + - "b" + - "a" + - {} + instruction_params: + array_create_params: + - dest: 3 + elements: [2, 4] + - dest: 1 + elements: [5, 6] + - dest: 0 + elements: [7] + object_create_params: + - dest: 6 + template_literal_idx: 5 + literal_key_fields: [] + fields: + - [8, 3] + - dest: 7 + template_literal_idx: 5 + literal_key_fields: [] + fields: + - [9, 1] + instructions: + - "Load { dest: 5, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Load { dest: 4, literal_idx: 2 }" + - "ArrayCreate { params_index: 0 }" + - "Load { dest: 8, literal_idx: 3 }" + - "ObjectCreate { params_index: 0 }" + - "ArrayCreate { params_index: 1 }" + - "Load { dest: 9, literal_idx: 4 }" + - "ObjectCreate { params_index: 1 }" + - "ArrayCreate { params_index: 2 }" + - "Return { value: 0 }" + want_result: [{"a": [1, {"b": [2, 3]}]}] diff --git a/tests/rvm/vm/suites/set_operations.yaml b/tests/rvm/vm/suites/set_operations.yaml new file mode 100644 index 0000000..a0ae811 --- /dev/null +++ b/tests/rvm/vm/suites/set_operations.yaml @@ -0,0 +1,284 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Set Operations Test Suite +# Tests set creation, deduplication, and membership testing +# Covers complex value deduplication, nested sets, and undefined handling + +cases: + - note: set_deduplication_simple + description: Set automatically deduplicates simple values + example_rego: "{1, 2, 1, 3, 2}" + literals: + - 1 + - 2 + - 3 + instructions: + - "SetNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" # 1 + - "SetAdd { set: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" # 2 + - "SetAdd { set: 0, value: 2 }" + - "SetAdd { set: 0, value: 1 }" # 1 again (duplicate) + - "Load { dest: 3, literal_idx: 2 }" # 3 + - "SetAdd { set: 0, value: 3 }" + - "SetAdd { set: 0, value: 2 }" # 2 again (duplicate) + - "Return { value: 0 }" + want_result: + set!: + - 1 + - 2 + - 3 + + - note: set_deduplication_objects + description: Set deduplicates identical objects + example_rego: "{{\"a\": 1}, {\"b\": 2}, {\"a\": 1}}" + literals: + - {"a": 1} + - {"b": 2} + instructions: + - "SetNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" # {"a": 1} + - "SetAdd { set: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" # {"b": 2} + - "SetAdd { set: 0, value: 2 }" + - "SetAdd { set: 0, value: 1 }" # {"a": 1} again (duplicate) + - "Return { value: 0 }" + want_result: + set!: + - {"a": 1} + - {"b": 2} + + - note: set_deduplication_arrays + description: Set deduplicates identical arrays + example_rego: "{[1, 2], [3, 4], [1, 2]}" + literals: + - [1, 2] + - [3, 4] + instructions: + - "SetNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" # [1, 2] + - "SetAdd { set: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" # [3, 4] + - "SetAdd { set: 0, value: 2 }" + - "SetAdd { set: 0, value: 1 }" # [1, 2] again (duplicate) + - "Return { value: 0 }" + want_result: + set!: + - [1, 2] + - [3, 4] + + - note: set_with_mixed_types + description: Set can contain different types + example_rego: "{1, \"text\", true, null, [1, 2], {\"a\": 1}}" + literals: + - 1 + - "text" + - [1, 2] + - {"a": 1} + instructions: + - "SetNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" # 1 + - "SetAdd { set: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" # "text" + - "SetAdd { set: 0, value: 2 }" + - "LoadTrue { dest: 3 }" + - "SetAdd { set: 0, value: 3 }" + - "LoadNull { dest: 4 }" + - "SetAdd { set: 0, value: 4 }" + - "Load { dest: 5, literal_idx: 2 }" # [1, 2] + - "SetAdd { set: 0, value: 5 }" + - "Load { dest: 6, literal_idx: 3 }" # {"a": 1} + - "SetAdd { set: 0, value: 6 }" + - "Return { value: 0 }" + want_result: + set!: + - 1 + - "text" + - true + - null + - [1, 2] + - {"a": 1} + + - note: set_contains_simple + description: Contains check on set with simple values + example_rego: "2 in {1, 2, 3}" + literals: + - 1 + - 2 + - 3 + instructions: + - "SetNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "SetAdd { set: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "SetAdd { set: 0, value: 2 }" + - "Load { dest: 3, literal_idx: 2 }" + - "SetAdd { set: 0, value: 3 }" + - "Contains { dest: 4, collection: 0, value: 2 }" + - "Return { value: 4 }" + want_result: true + + - note: set_contains_object + description: Contains check with object value + example_rego: "{\"a\": 1} in {{\"a\": 1}, {\"b\": 2}}" + literals: + - {"a": 1} + - {"b": 2} + instructions: + - "SetNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "SetAdd { set: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "SetAdd { set: 0, value: 2 }" + - "Contains { dest: 3, collection: 0, value: 1 }" + - "Return { value: 3 }" + want_result: true + + - note: set_contains_not_found + description: Contains returns false when value not in set + example_rego: "5 in {1, 2, 3}" + literals: + - 1 + - 2 + - 3 + - 5 + instructions: + - "SetNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "SetAdd { set: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "SetAdd { set: 0, value: 2 }" + - "Load { dest: 3, literal_idx: 2 }" + - "SetAdd { set: 0, value: 3 }" + - "Load { dest: 4, literal_idx: 3 }" + - "Contains { dest: 5, collection: 0, value: 4 }" + - "Return { value: 5 }" + want_result: false + + - note: set_create_from_registers + description: SetCreate instruction creates set from multiple registers + literals: + - 1 + - 2 + - 3 + instruction_params: + set_create_params: + - dest: 0 + elements: [1, 2, 3, 1] # Duplicate 1 + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Load { dest: 3, literal_idx: 2 }" + - "SetCreate { params_index: 0 }" + - "Return { value: 0 }" + want_result: + set!: + - 1 + - 2 + - 3 + + - note: set_create_with_undefined_element + description: SetCreate with undefined element returns undefined (short-circuit) + literals: + - 1 + - 2 + - "#undefined" + instruction_params: + set_create_params: + - dest: 0 + elements: [1, 2, 3] # r3 is undefined + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "Load { dest: 3, literal_idx: 2 }" # r3 is explicitly undefined + - "SetCreate { params_index: 0 }" + - "Return { value: 0 }" + want_result: "#undefined" + + - note: set_large_collection + description: Set with many elements (stress test) + literals: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + instructions: + - "SetNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "SetAdd { set: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "SetAdd { set: 0, value: 2 }" + - "Load { dest: 3, literal_idx: 2 }" + - "SetAdd { set: 0, value: 3 }" + - "Load { dest: 4, literal_idx: 3 }" + - "SetAdd { set: 0, value: 4 }" + - "Load { dest: 5, literal_idx: 4 }" + - "SetAdd { set: 0, value: 5 }" + - "Load { dest: 6, literal_idx: 5 }" + - "SetAdd { set: 0, value: 6 }" + - "Load { dest: 7, literal_idx: 6 }" + - "SetAdd { set: 0, value: 7 }" + - "Load { dest: 8, literal_idx: 7 }" + - "SetAdd { set: 0, value: 8 }" + - "Load { dest: 9, literal_idx: 8 }" + - "SetAdd { set: 0, value: 9 }" + - "Load { dest: 10, literal_idx: 9 }" + - "SetAdd { set: 0, value: 10 }" + - "Return { value: 0 }" + want_result: + set!: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + + - note: set_empty + description: Empty set creation + example_rego: "set()" + literals: [] + instructions: + - "SetNew { dest: 0 }" + - "Return { value: 0 }" + want_result: + set!: [] + + - note: set_nested_in_set + description: Set containing other sets + example_rego: "{{1, 2}, {3, 4}}" + literals: + - set!: + - 1 + - 2 + - set!: + - 3 + - 4 + instructions: + - "SetNew { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "SetAdd { set: 0, value: 1 }" + - "Load { dest: 2, literal_idx: 1 }" + - "SetAdd { set: 0, value: 2 }" + - "Return { value: 0 }" + want_result: + set!: + - + set!: + - 1 + - 2 + - + set!: + - 3 + - 4 diff --git a/tests/rvm/vm/suites/type_errors.yaml b/tests/rvm/vm/suites/type_errors.yaml new file mode 100644 index 0000000..1181391 --- /dev/null +++ b/tests/rvm/vm/suites/type_errors.yaml @@ -0,0 +1,224 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Type Errors Test Suite +# Tests graceful error handling for type mismatches across instruction families +# Verifies that VM returns appropriate errors instead of panicking + +cases: + # Mixed-type arithmetic operations + - note: arithmetic_add_string_to_int + description: Adding string to int should error gracefully + example_rego: "1 + \"text\"" + literals: + - 1 + - "text" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Add { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_error: "Cannot add" + + - note: arithmetic_mul_bool_by_int + description: Multiplying boolean by int should error + example_rego: "true * 5" + literals: + - 5 + instructions: + - "LoadTrue { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Mul { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_error: "Cannot multiply" + + - note: arithmetic_div_null_by_int + description: Dividing null by int should error + example_rego: "null / 2" + literals: + - 2 + instructions: + - "LoadNull { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Div { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_error: "Cannot divide" + + - note: arithmetic_sub_array_from_int + description: Subtracting array from int should error + example_rego: "10 - [1, 2]" + literals: + - 10 + - [1, 2] + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Sub { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_error: "Cannot subtract" + + - note: arithmetic_add_object_to_int + description: Adding object to int should error + example_rego: "5 + {\"a\": 1}" + literals: + - 5 + - {"a": 1} + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Add { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_error: "Cannot add" + + # Logical operations on non-booleans + - note: logical_and_string_with_bool + description: AND with string operand should error + example_rego: "\"text\" && true" + literals: + - "text" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "LoadTrue { dest: 1 }" + - "And { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_error: "#undefined" + + - note: logical_or_int_with_bool + description: OR with int operand should error + example_rego: "42 || false" + literals: + - 42 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "LoadFalse { dest: 1 }" + - "Or { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_error: "#undefined" + + - note: logical_not_int + description: NOT with int operand should error + example_rego: "!42" + literals: + - 42 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Not { dest: 1, operand: 0 }" + - "Return { value: 1 }" + want_error: "#undefined" + + # Invalid indexing operations + - note: index_int_with_string_key + description: Indexing int with string should return undefined + example_rego: "123[\"key\"]" + literals: + - 123 + - "key" + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" + + - note: index_null_with_int + description: Indexing null should return undefined + example_rego: "null[0]" + literals: + - 0 + instructions: + - "LoadNull { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" + + - note: index_bool_with_string + description: Indexing boolean should return undefined + example_rego: "true.field" + literals: + - "field" + instructions: + - "LoadTrue { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" + + - note: index_string_with_object_key + description: Indexing string with object key should return undefined + example_rego: "\"text\"[{}]" + literals: + - "text" + - {} + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Index { dest: 2, container: 0, key: 1 }" + - "Return { value: 2 }" + want_result: "#undefined" + + # Contains with incompatible types + - note: contains_in_int + description: Contains check on int should return false + example_rego: "1 in 123" + literals: + - 123 + - 1 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Contains { dest: 2, collection: 0, value: 1 }" + - "Return { value: 2 }" + want_result: false + + - note: contains_in_bool + description: Contains check on boolean should return false + example_rego: "1 in true" + literals: + - 1 + instructions: + - "LoadTrue { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Contains { dest: 2, collection: 0, value: 1 }" + - "Return { value: 2 }" + want_result: false + + - note: contains_in_null + description: Contains check on null should return false + example_rego: "1 in null" + literals: + - 1 + instructions: + - "LoadNull { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Contains { dest: 2, collection: 0, value: 1 }" + - "Return { value: 2 }" + want_result: false + + # Comparison operations with incompatible types + - note: compare_string_lt_int + description: Comparing string < int returns ordering result in non-strict; errors in strict + example_rego: "\"abc\" < 5" + literals: + - "abc" + - 5 + instructions: + - "Load { dest: 0, literal_idx: 0 }" + - "Load { dest: 1, literal_idx: 1 }" + - "Lt { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: false + want_error_strict: "#undefined" + + - note: compare_bool_gt_array + description: Comparing bool > array returns ordering result in non-strict; errors in strict + example_rego: "true > [1, 2]" + literals: + - [1, 2] + instructions: + - "LoadTrue { dest: 0 }" + - "Load { dest: 1, literal_idx: 0 }" + - "Gt { dest: 2, left: 0, right: 1 }" + - "Return { value: 2 }" + want_result: false + want_error_strict: "#undefined" diff --git a/tests/rvm/vm/suites/virtual_data_lookup.yaml b/tests/rvm/vm/suites/virtual_data_lookup.yaml new file mode 100644 index 0000000..c5ab02f --- /dev/null +++ b/tests/rvm/vm/suites/virtual_data_lookup.yaml @@ -0,0 +1,274 @@ +# Virtual Data Lookup Test Suite +# Exercises VirtualDataDocumentLookup across data/rule blending and errors. + +cases: + - note: virtual_lookup_base_data_only + description: Lookup resolves to base data when no rules override + data: + services: + api: + enabled: true + literals: + - "services" + - "api" + - "enabled" + instruction_params: + virtual_data_document_lookup_params: + - dest: 3 + path_components: + - literal_idx: 0 + - literal_idx: 1 + - literal_idx: 2 + instructions: + - "VirtualDataDocumentLookup { params_index: 0 }" + - "Return { value: 3 }" + want_result: true + + - note: virtual_lookup_rule_override + description: Lookup combines rule result overriding base data + data: + services: + api: + enabled: false + literals: + - "services" + - "api" + - "enabled" + rule_infos: + - rule_type: Complete + definitions: + - [2] + rule_tree: + data: + services: + api: + enabled: 0 + instruction_params: + virtual_data_document_lookup_params: + - dest: 3 + path_components: + - literal_idx: 0 + - literal_idx: 1 + - literal_idx: 2 + instructions: + - "VirtualDataDocumentLookup { params_index: 0 }" + - "Return { value: 3 }" + - "RuleInit { result_reg: 1, rule_index: 0 }" + - "LoadTrue { dest: 1 }" + - "RuleReturn {}" + want_result: true + + - note: virtual_lookup_rule_subobject + description: Lookup merges nested rule tree subobjects + data: + tenants: + alpha: + feature: "beta" + literals: + - "tenants" + - "alpha" + - "feature" + rule_infos: + - rule_type: Complete + definitions: + - [2] + rule_tree: + data: + tenants: + alpha: + extra: 0 + instruction_params: + virtual_data_document_lookup_params: + - dest: 4 + path_components: + - literal_idx: 0 + - literal_idx: 1 + instructions: + - "VirtualDataDocumentLookup { params_index: 0 }" + - "Return { value: 4 }" + - "RuleInit { result_reg: 1, rule_index: 0 }" + - "Load { dest: 1, literal_idx: 2 }" + - "RuleReturn {}" + want_result: + feature: "beta" + extra: "feature" + + - note: virtual_lookup_invalid_rule_index + description: Invalid rule index produces corresponding error + literals: + - "services" + instruction_params: + virtual_data_document_lookup_params: + - dest: 1 + path_components: + - literal_idx: 0 + rule_tree: + data: + services: 99 + instructions: + - "VirtualDataDocumentLookup { params_index: 0 }" + want_error: "Rule index 99 out of bounds" + + - note: virtual_lookup_rule_data_conflict + description: Rule completely replaces data at the same path + data: + config: + mode: "data_value" + literals: + - "config" + - "mode" + - "rule_value" + rule_infos: + - rule_type: Complete + definitions: + - [3] + rule_tree: + data: + config: + mode: 0 + instruction_params: + virtual_data_document_lookup_params: + - dest: 3 + path_components: + - literal_idx: 0 + - literal_idx: 1 + instructions: + - "VirtualDataDocumentLookup { params_index: 0 }" + - "Return { value: 3 }" + - "RuleInit { result_reg: 1, rule_index: 0 }" + - "Load { dest: 1, literal_idx: 2 }" + - "RuleReturn {}" + want_result: "rule_value" + + - note: virtual_lookup_deep_path_5_levels + description: Lookup with 5-level deep path + data: + level1: + level2: + level3: + level4: + level5: "deep_value" + literals: + - "level1" + - "level2" + - "level3" + - "level4" + - "level5" + instruction_params: + virtual_data_document_lookup_params: + - dest: 5 + path_components: + - literal_idx: 0 + - literal_idx: 1 + - literal_idx: 2 + - literal_idx: 3 + - literal_idx: 4 + instructions: + - "VirtualDataDocumentLookup { params_index: 0 }" + - "Return { value: 5 }" + want_result: "deep_value" + + - note: virtual_lookup_deep_path_with_array_indices + description: Lookup path with array index components + data: + containers: + - name: "first" + - name: "second" + - name: "third" + literals: + - "containers" + - 1 + - "name" + instruction_params: + virtual_data_document_lookup_params: + - dest: 3 + path_components: + - literal_idx: 0 + - literal_idx: 1 + - literal_idx: 2 + instructions: + - "VirtualDataDocumentLookup { params_index: 0 }" + - "Return { value: 3 }" + want_result: "second" + + - note: virtual_lookup_mixed_data_rules_deep + description: Deep lookup with rules at multiple levels + data: + org: + dept: + team: + lead: "data_lead" + literals: + - "org" + - "dept" + - "team" + - "lead" + - "rule_lead" + rule_infos: + - rule_type: Complete + definitions: + - [3] + rule_tree: + data: + org: + dept: + team: + lead: 0 + instruction_params: + virtual_data_document_lookup_params: + - dest: 5 + path_components: + - literal_idx: 0 + - literal_idx: 1 + - literal_idx: 2 + - literal_idx: 3 + instructions: + - "VirtualDataDocumentLookup { params_index: 0 }" + - "Return { value: 5 }" + - "RuleInit { result_reg: 1, rule_index: 0 }" + - "Load { dest: 1, literal_idx: 4 }" + - "RuleReturn {}" + want_result: "rule_lead" + + - note: virtual_lookup_nonexistent_deep_path + description: Lookup of non-existent deep path returns undefined + data: + root: + child: "value" + literals: + - "root" + - "nonexistent" + - "path" + instruction_params: + virtual_data_document_lookup_params: + - dest: 3 + path_components: + - literal_idx: 0 + - literal_idx: 1 + - literal_idx: 2 + instructions: + - "VirtualDataDocumentLookup { params_index: 0 }" + - "Return { value: 3 }" + want_result: "#undefined" + + - note: virtual_lookup_partial_match_deep_path + description: Partial path match returns undefined (not intermediate object) + data: + a: + b: + c: "value" + literals: + - "a" + - "b" + - "d" + instruction_params: + virtual_data_document_lookup_params: + - dest: 3 + path_components: + - literal_idx: 0 + - literal_idx: 1 + - literal_idx: 2 + instructions: + - "VirtualDataDocumentLookup { params_index: 0 }" + - "Return { value: 3 }" + want_result: "#undefined"