mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
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 <anakrish@microsoft.com> * 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 <anakrish@microsoft.com> --------- Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
committed by
GitHub
parent
6dc505c88b
commit
49bd3c22f3
128
Cargo.lock
generated
128
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
108
bindings/ffi/Cargo.lock
generated
108
bindings/ffi/Cargo.lock
generated
@@ -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",
|
||||
|
||||
100
bindings/java/Cargo.lock
generated
100
bindings/java/Cargo.lock
generated
@@ -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",
|
||||
|
||||
100
bindings/python/Cargo.lock
generated
100
bindings/python/Cargo.lock
generated
@@ -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",
|
||||
|
||||
100
bindings/wasm/Cargo.lock
generated
100
bindings/wasm/Cargo.lock
generated
@@ -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",
|
||||
|
||||
274
docs/rvm/architecture.md
Normal file
274
docs/rvm/architecture.md
Normal file
@@ -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<Instruction>` | Ordered bytecode emitted by the compiler. | Each opcode is defined in `src/rvm/instructions/mod.rs` and executed by the dispatch tree. |
|
||||
| `literals: Vec<Value>` | 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<BuiltinInfo>` | Metadata for builtin calls. | Enforced and resolved by `Program::initialize_resolved_builtins`. |
|
||||
| `entry_points: IndexMap<String, usize>` | Maps path names (e.g. `data.pkg.rule`) to starting PCs. | Preserves declaration order for tooling and serialized in the artifact section. |
|
||||
| `sources: Vec<SourceFile>` | Captures original policy sources. | Stored in the stable artifact section alongside entry-points. |
|
||||
| `rule_infos: Vec<RuleInfo>` | Metadata for every rule. | Includes register windows, default values, destructuring blocks. |
|
||||
| `instruction_spans: Vec<Option<SpanInfo>>` | 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<BuiltinFcn>` | 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)
|
||||
238
docs/rvm/instruction-set.md
Normal file
238
docs/rvm/instruction-set.md
Normal file
@@ -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<LiteralOrRegister>`
|
||||
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<u8>` | Registers providing array elements (order preserved). |
|
||||
| `SetCreateParams` | `dest` | Destination register for the set literal. |
|
||||
| | `elements: Vec<u8>` | Registers providing set members (duplicates dropped at runtime). |
|
||||
| `VirtualDataDocumentLookupParams` | `dest` | Destination register for lookup result. |
|
||||
| | `path_components: Vec<LiteralOrRegister>` | 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<LiteralOrRegister>` | 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.
|
||||
|
||||
---
|
||||
|
||||
243
docs/rvm/vm-runtime.md
Normal file
243
docs/rvm/vm-runtime.md
Normal file
@@ -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<Value>` | Active register window for the current frame. | `execution.rs`, `dispatch.rs` |
|
||||
| `pc: usize` | Instruction pointer in run-to-completion mode. | `execution.rs` |
|
||||
| `program: Arc<Program>` | 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
280
src/rvm/instructions/display.rs
Normal file
280
src/rvm/instructions/display.rs
Normal file
@@ -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::<Vec<_>>()
|
||||
.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::<Vec<_>>()
|
||||
.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::<Vec<_>>()
|
||||
.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)
|
||||
}
|
||||
}
|
||||
381
src/rvm/instructions/mod.rs
Normal file
381
src/rvm/instructions/mod.rs
Normal file
@@ -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<u8>,
|
||||
},
|
||||
|
||||
/// 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 {}
|
||||
}
|
||||
}
|
||||
445
src/rvm/instructions/params.rs
Normal file
445
src/rvm/instructions/params.rs
Normal file
@@ -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<u8>,
|
||||
}
|
||||
|
||||
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<u8>,
|
||||
}
|
||||
|
||||
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<LiteralOrRegister>,
|
||||
}
|
||||
|
||||
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<u16> {
|
||||
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<u8> {
|
||||
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<LiteralOrRegister>,
|
||||
}
|
||||
|
||||
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<u16> {
|
||||
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<u8> {
|
||||
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<LoopStartParams>,
|
||||
/// Builtin function call parameter table for BuiltinCall instructions
|
||||
pub builtin_call_params: Vec<BuiltinCallParams>,
|
||||
/// Function rule call parameter table for FunctionCall instructions
|
||||
pub function_call_params: Vec<FunctionCallParams>,
|
||||
/// Object creation parameter table for ObjectCreate instructions
|
||||
pub object_create_params: Vec<ObjectCreateParams>,
|
||||
/// Array creation parameter table for ArrayCreate instructions
|
||||
pub array_create_params: Vec<ArrayCreateParams>,
|
||||
/// Set creation parameter table for SetCreate instructions
|
||||
pub set_create_params: Vec<SetCreateParams>,
|
||||
/// Virtual data document lookup parameter table for VirtualDataDocumentLookup instructions
|
||||
pub virtual_data_document_lookup_params: Vec<VirtualDataDocumentLookupParams>,
|
||||
/// Chained index parameter table for ChainedIndex instructions
|
||||
pub chained_index_params: Vec<ChainedIndexParams>,
|
||||
/// Comprehension parameter table for ComprehensionBegin instructions
|
||||
pub comprehension_begin_params: Vec<ComprehensionBeginParams>,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
47
src/rvm/instructions/types.rs
Normal file
47
src/rvm/instructions/types.rs
Normal file
@@ -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,
|
||||
}
|
||||
16
src/rvm/mod.rs
Normal file
16
src/rvm/mod.rs
Normal file
@@ -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;
|
||||
322
src/rvm/program/core.rs
Normal file
322
src/rvm/program/core.rs
Normal file
@@ -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<Instruction>,
|
||||
|
||||
/// Literal value table (skipped in serde, serialized separately as JSON)
|
||||
#[serde(skip, default = "Vec::new")]
|
||||
pub literals: Vec<Value>,
|
||||
|
||||
/// Complex instruction parameter data (for LoopStart, Call, etc.)
|
||||
pub instruction_data: InstructionData,
|
||||
|
||||
/// Builtin function information table
|
||||
pub builtin_info_table: Vec<BuiltinInfo>,
|
||||
|
||||
/// Entry points mapping with preserved insertion order (skipped in serde, serialized separately as JSON)
|
||||
#[serde(skip, default = "IndexMap::new")]
|
||||
pub entry_points: IndexMap<String, usize>,
|
||||
|
||||
/// Source files table with content (skipped in serde, serialized separately as JSON)
|
||||
#[serde(skip, default = "Vec::new")]
|
||||
pub sources: Vec<SourceFile>,
|
||||
|
||||
/// Rule metadata: rule_index -> rule information
|
||||
pub rule_infos: Vec<RuleInfo>,
|
||||
|
||||
/// Span information for each instruction (for debugging)
|
||||
pub instruction_spans: Vec<Option<SpanInfo>>,
|
||||
|
||||
/// 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<BuiltinFcn>,
|
||||
|
||||
/// 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<F>(&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<F>(&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<SpanInfo>) {
|
||||
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<usize> {
|
||||
self.entry_points.get(path).copied()
|
||||
}
|
||||
|
||||
/// Get all entry points as IndexMap
|
||||
pub fn get_entry_points(&self) -> &IndexMap<String, usize> {
|
||||
&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()
|
||||
}
|
||||
}
|
||||
964
src/rvm/program/listing.rs
Normal file
964
src/rvm/program/listing.rs
Normal file
@@ -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<u16> = None;
|
||||
|
||||
// Track active loops and comprehensions by their end addresses
|
||||
let mut active_ends: Vec<u16> = 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(|_| "<invalid>".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(|_| "<invalid>".to_string());
|
||||
format!("Load literal: {}", literal_json)
|
||||
} else {
|
||||
"Load literal: <invalid index>".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::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let builtin_name = program
|
||||
.builtin_info_table
|
||||
.get(params.builtin_index as usize)
|
||||
.map(|info| info.name.as_str())
|
||||
.unwrap_or("<invalid>");
|
||||
|
||||
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::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let func_name = program
|
||||
.rule_infos
|
||||
.get(params.func_rule_index as usize)
|
||||
.map(|info| info.name.as_str())
|
||||
.unwrap_or("<invalid>");
|
||||
|
||||
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(|_| "<invalid>".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::<Vec<_>>()
|
||||
.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 <invalid params P{}>", 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::<Vec<_>>()
|
||||
.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 <invalid params P{}>", 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("<invalid>");
|
||||
|
||||
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("<invalid>");
|
||||
|
||||
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<String> = 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")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/rvm/program/mod.rs
Normal file
19
src/rvm/program/mod.rs
Normal file
@@ -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,
|
||||
};
|
||||
22
src/rvm/program/recompile.rs
Normal file
22
src/rvm/program/recompile.rs
Normal file
@@ -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<Program, String> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
105
src/rvm/program/rule_tree.rs
Normal file
105
src/rvm/program/rule_tree.rs
Normal file
@@ -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<String>,
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
||||
482
src/rvm/program/serialization/binary.rs
Normal file
482
src/rvm/program/serialization/binary.rs
Normal file
@@ -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<String, usize>, Vec<SourceFile>, 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<Vec<u8>, 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<ArtifactData, String> {
|
||||
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<DeserializationResult, 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 - 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::<Program, _>(
|
||||
&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::<serde_json::Value>(
|
||||
&data[json_start..json_start + json_len],
|
||||
) {
|
||||
Ok(combined) => {
|
||||
let literals = combined
|
||||
.get("literals")
|
||||
.and_then(|v| serde_json::from_value::<Vec<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::<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::<Vec<BinaryValue>, _>(
|
||||
&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::<BinaryValue, _>(
|
||||
&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::<Program, _>(
|
||||
&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<bool, String> {
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
196
src/rvm/program/serialization/json.rs
Normal file
196
src/rvm/program/serialization/json.rs
Normal file
@@ -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<String, String> {
|
||||
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<Program, String> {
|
||||
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<Instruction> = 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<Value> = json_data
|
||||
.get("literals")
|
||||
.map(|v| serde_json::from_value(v.clone()).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
let builtin_info_table: Vec<BuiltinInfo> = json_data
|
||||
.get("builtin_info_table")
|
||||
.map(|v| serde_json::from_value(v.clone()).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
let entry_points: IndexMap<String, usize> = json_data
|
||||
.get("entry_points")
|
||||
.map(|v| serde_json::from_value(v.clone()).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
let sources: Vec<SourceFile> = json_data
|
||||
.get("sources")
|
||||
.map(|v| serde_json::from_value(v.clone()).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
let rule_infos: Vec<RuleInfo> = json_data
|
||||
.get("rule_infos")
|
||||
.map(|v| serde_json::from_value(v.clone()).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
let instruction_spans: Vec<Option<SpanInfo>> = 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)
|
||||
}
|
||||
}
|
||||
29
src/rvm/program/serialization/mod.rs
Normal file
29
src/rvm/program/serialization/mod.rs
Normal file
@@ -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),
|
||||
}
|
||||
294
src/rvm/program/serialization/value.rs
Normal file
294
src/rvm/program/serialization/value.rs
Normal file
@@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<Value>);
|
||||
|
||||
impl<'a> Serialize for BinarySetRef<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<Value, Value>);
|
||||
|
||||
impl<'a> Serialize for BinaryObjectRef<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<A>(self, data: A) -> Result<Self::Value, A::Error>
|
||||
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::<bool>()?;
|
||||
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::<i64>()?;
|
||||
Ok(BinaryValue(Value::from(value)))
|
||||
}
|
||||
(BinaryVariant::NumberU64, variant) => {
|
||||
let value = variant.newtype_variant::<u64>()?;
|
||||
Ok(BinaryValue(Value::from(value)))
|
||||
}
|
||||
(BinaryVariant::NumberF64, variant) => {
|
||||
let value = variant.newtype_variant::<f64>()?;
|
||||
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<BinaryValue> = variant.newtype_variant()?;
|
||||
let values: Vec<Value> = items.into_iter().map(BinaryValue::into_value).collect();
|
||||
Ok(BinaryValue(Value::from(values)))
|
||||
}
|
||||
(BinaryVariant::Set, variant) => {
|
||||
let items: Vec<BinaryValue> = 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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_enum("BinaryValue", BINARY_VARIANTS, BinaryValueVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn binaries_to_values(binaries: Vec<BinaryValue>) -> Result<Vec<Value>, String> {
|
||||
Ok(binaries.into_iter().map(BinaryValue::into_value).collect())
|
||||
}
|
||||
|
||||
pub(crate) fn binary_to_value(binary: BinaryValue) -> Result<Value, String> {
|
||||
Ok(binary.into_value())
|
||||
}
|
||||
182
src/rvm/program/types.rs
Normal file
182
src/rvm/program/types.rs
Normal file
@@ -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<String>,
|
||||
/// 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<Vec<Vec<u32>>>,
|
||||
/// Function-specific information (only present for function rules)
|
||||
pub function_info: Option<FunctionInfo>,
|
||||
/// Index into the program's literal table for default value (only for Complete rules)
|
||||
pub default_literal_index: Option<u16>,
|
||||
/// 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<Option<u32>>,
|
||||
}
|
||||
|
||||
impl RuleInfo {
|
||||
pub fn new(
|
||||
name: String,
|
||||
rule_type: RuleType,
|
||||
definitions: crate::Rc<Vec<Vec<u32>>>,
|
||||
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<Vec<Vec<u32>>>,
|
||||
param_names: Vec<String>,
|
||||
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,
|
||||
}
|
||||
648
src/rvm/tests/instruction_parser.rs
Normal file
648
src/rvm/tests/instruction_parser.rs
Normal file
@@ -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<Instruction> {
|
||||
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<Vec<(String, String)>> {
|
||||
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<u16> {
|
||||
for (key, value) in params {
|
||||
if key == name {
|
||||
return value
|
||||
.parse::<u16>()
|
||||
.map_err(|_| anyhow!("Invalid u16 value for {}: {}", name, value));
|
||||
}
|
||||
}
|
||||
bail!("Missing parameter: {}", name);
|
||||
}
|
||||
|
||||
fn get_param_bool(params: &[(String, String)], name: &str) -> Result<bool> {
|
||||
for (key, value) in params {
|
||||
if key == name {
|
||||
return value
|
||||
.parse::<bool>()
|
||||
.map_err(|_| anyhow!("Invalid bool value for {}: {}", name, value));
|
||||
}
|
||||
}
|
||||
bail!("Missing parameter: {}", name);
|
||||
}
|
||||
|
||||
pub fn parse_loop_mode(text: &str) -> Result<LoopMode> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
let _params = parse_params(params_text)?;
|
||||
Ok(Instruction::RuleReturn {})
|
||||
}
|
||||
|
||||
fn parse_destructuring_success(params_text: &str) -> Result<Instruction> {
|
||||
let _params = parse_params(params_text)?;
|
||||
Ok(Instruction::DestructuringSuccess {})
|
||||
}
|
||||
|
||||
fn parse_object_set(params_text: &str) -> Result<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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<Instruction> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
12
src/rvm/tests/mod.rs
Normal file
12
src/rvm/tests/mod.rs
Normal file
@@ -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;
|
||||
104
src/rvm/tests/test_utils.rs
Normal file
104
src/rvm/tests/test_utils.rs
Normal file
@@ -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::<Vec<BinaryValue>, _>(
|
||||
&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()
|
||||
))
|
||||
}
|
||||
}
|
||||
995
src/rvm/tests/vm.rs
Normal file
995
src/rvm/tests/vm.rs
Normal file
@@ -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<Vec<u16>>,
|
||||
#[serde(default)]
|
||||
default_rule_index: Option<u16>,
|
||||
#[serde(default)]
|
||||
default_literal_index: Option<u16>,
|
||||
#[serde(default)]
|
||||
destructuring_blocks: Option<Vec<Option<u16>>>,
|
||||
}
|
||||
|
||||
#[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<crate::Value>,
|
||||
#[serde(default)]
|
||||
values: Vec<crate::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct VmTestCase {
|
||||
note: String,
|
||||
#[serde(default)]
|
||||
description: Option<String>,
|
||||
#[serde(default)]
|
||||
example_rego: Option<String>,
|
||||
#[serde(default)]
|
||||
data: Option<crate::Value>,
|
||||
#[serde(default)]
|
||||
input: Option<crate::Value>,
|
||||
literals: Vec<crate::Value>,
|
||||
#[serde(default)]
|
||||
rule_infos: Vec<RuleInfoSpec>,
|
||||
#[serde(default)]
|
||||
default_rules: Vec<DefaultRuleSpec>,
|
||||
#[serde(default)]
|
||||
rule_tree: Option<crate::Value>,
|
||||
#[serde(default)]
|
||||
instruction_params: Option<InstructionParamsSpec>,
|
||||
#[serde(default)]
|
||||
max_instructions: Option<usize>,
|
||||
#[serde(default)]
|
||||
host_await_responses: Option<Vec<HostAwaitResponseSpec>>,
|
||||
#[serde(default)]
|
||||
host_await_responses_run_to_completion: Option<Vec<HostAwaitResponseSpec>>,
|
||||
#[serde(default)]
|
||||
host_await_responses_suspendable: Option<Vec<HostAwaitResponseSpec>>,
|
||||
#[serde(default)]
|
||||
ignore_run_to_completion_hostawait_failure: bool,
|
||||
instructions: Vec<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_value")]
|
||||
want_result: Option<crate::Value>,
|
||||
#[serde(default)]
|
||||
want_error: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_value")]
|
||||
want_result_strict: Option<crate::Value>,
|
||||
#[serde(default)]
|
||||
want_error_strict: Option<String>,
|
||||
}
|
||||
|
||||
fn deserialize_optional_value<'de, D>(deserializer: D) -> Result<Option<crate::Value>, 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<LoopStartParamsSpec>,
|
||||
#[serde(default)]
|
||||
call_params: Vec<CallParamsSpec>,
|
||||
#[serde(default)]
|
||||
builtin_call_params: Vec<BuiltinCallParamsSpec>,
|
||||
#[serde(default)]
|
||||
function_call_params: Vec<FunctionCallParamsSpec>,
|
||||
#[serde(default)]
|
||||
builtin_infos: Vec<BuiltinInfoSpec>,
|
||||
#[serde(default)]
|
||||
object_create_params: Vec<ObjectCreateParamsSpec>,
|
||||
#[serde(default)]
|
||||
array_create_params: Vec<ArrayCreateParamsSpec>,
|
||||
#[serde(default)]
|
||||
set_create_params: Vec<SetCreateParamsSpec>,
|
||||
#[serde(default)]
|
||||
virtual_data_document_lookup_params: Vec<VirtualDataDocumentLookupParamsSpec>,
|
||||
#[serde(default)]
|
||||
chained_index_params: Vec<ChainedIndexParamsSpec>,
|
||||
#[serde(default, alias = "comprehension_start_params")]
|
||||
comprehension_begin_params: Vec<ComprehensionBeginParamsSpec>,
|
||||
}
|
||||
|
||||
#[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<u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct FunctionCallParamsSpec {
|
||||
func: u16,
|
||||
dest: u16,
|
||||
args: Vec<u16>,
|
||||
}
|
||||
|
||||
#[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<u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct SetCreateParamsSpec {
|
||||
dest: u16,
|
||||
elements: Vec<u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct LiteralOrRegisterSpec {
|
||||
#[serde(default, alias = "literal")]
|
||||
literal_idx: Option<u16>,
|
||||
#[serde(default, alias = "reg")]
|
||||
register: Option<u16>,
|
||||
}
|
||||
|
||||
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<LiteralOrRegisterSpec>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct ChainedIndexParamsSpec {
|
||||
dest: u16,
|
||||
root: u16,
|
||||
path_components: Vec<LiteralOrRegisterSpec>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct ComprehensionBeginParamsSpec {
|
||||
mode: String,
|
||||
collection_reg: u16,
|
||||
#[serde(default)]
|
||||
result_reg: Option<u16>,
|
||||
key_reg: u16,
|
||||
value_reg: u16,
|
||||
body_start: u16,
|
||||
comprehension_end: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct VmTestSuite {
|
||||
cases: Vec<VmTestCase>,
|
||||
}
|
||||
|
||||
/// Execute VM instructions directly from parsed instructions and literals
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn execute_vm_instructions(
|
||||
instructions: Vec<crate::rvm::instructions::Instruction>,
|
||||
literals: Vec<Value>,
|
||||
rule_infos: Vec<RuleInfoSpec>,
|
||||
rule_tree: Option<Value>,
|
||||
instruction_params: Option<InstructionParamsSpec>,
|
||||
data: Option<Value>,
|
||||
input: Option<Value>,
|
||||
max_instructions: Option<usize>,
|
||||
host_await_responses: Option<Vec<HostAwaitResponseSpec>>,
|
||||
host_await_responses_run_to_completion: Option<Vec<HostAwaitResponseSpec>>,
|
||||
host_await_responses_suspendable: Option<Vec<HostAwaitResponseSpec>>,
|
||||
ignore_run_to_completion_hostawait_failure: bool,
|
||||
strict: bool,
|
||||
) -> Result<Value> {
|
||||
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<HostAwaitResponseSpec>| -> Result<BTreeMap<Value, Vec<Value>>> {
|
||||
let mut processed: BTreeMap<Value, Vec<Value>> = BTreeMap::new();
|
||||
|
||||
for spec in responses {
|
||||
let identifier = process_value(&spec.id)?;
|
||||
let mut values: Vec<Value> = 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<Vec<u16>> to Vec<Vec<u32>>
|
||||
let definitions: Vec<Vec<u32>> = rule_info_spec
|
||||
.definitions
|
||||
.iter()
|
||||
.map(|def| def.iter().map(|&x| x as u32).collect())
|
||||
.collect();
|
||||
|
||||
let mut destructuring_blocks: Vec<Option<u32>> = 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<u16> 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<u16> 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<LiteralOrRegister> = 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<LiteralOrRegister> = 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<BTreeMap<Value, Vec<Value>>>|
|
||||
-> Result<Result<Value>> {
|
||||
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::<BTreeMap<_, _>>()
|
||||
});
|
||||
|
||||
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<Value>,
|
||||
other_name: &str,
|
||||
other: &Result<Value>|
|
||||
-> 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::<Result<Vec<_>>>()?;
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
101
src/rvm/vm/arithmetic.rs
Normal file
101
src/rvm/vm/arithmetic.rs
Normal file
@@ -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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<bool> {
|
||||
match value {
|
||||
Value::Bool(b) => Some(*b),
|
||||
Value::Null if !self.strict_builtin_errors => Some(true),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
543
src/rvm/vm/comprehension.rs
Normal file
543
src/rvm/vm/comprehension.rs
Normal file
@@ -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<u8>,
|
||||
) -> 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<u8>,
|
||||
) -> 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<u8>,
|
||||
) -> 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<ExecutionFrame> = 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())),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
97
src/rvm/vm/context.rs
Normal file
97
src/rvm/vm/context.rs
Normal file
@@ -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<Vec<Value>>,
|
||||
index: usize,
|
||||
},
|
||||
Object {
|
||||
obj: Rc<BTreeMap<Value, Value>>,
|
||||
current_key: Option<Value>,
|
||||
first_iteration: bool,
|
||||
},
|
||||
Set {
|
||||
items: Rc<BTreeSet<Value>>,
|
||||
current_item: Option<Value>,
|
||||
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<IterationState>,
|
||||
/// Resume location for the parent frame once this comprehension completes
|
||||
pub(super) resume_pc: usize,
|
||||
}
|
||||
771
src/rvm/vm/dispatch.rs
Normal file
771
src/rvm/vm/dispatch.rs
Normal file
@@ -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<InstructionOutcome> {
|
||||
self.execute_load_and_move(program, instruction)
|
||||
}
|
||||
|
||||
fn execute_load_and_move(
|
||||
&mut self,
|
||||
program: &Program,
|
||||
instruction: Instruction,
|
||||
) -> Result<InstructionOutcome> {
|
||||
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<InstructionOutcome> {
|
||||
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<InstructionOutcome> {
|
||||
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<InstructionOutcome> {
|
||||
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<InstructionOutcome> {
|
||||
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<Value> = 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<InstructionOutcome> {
|
||||
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<InstructionOutcome> {
|
||||
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
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/rvm/vm/errors.rs
Normal file
121
src/rvm/vm/errors.rs
Normal file
@@ -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<String>,
|
||||
},
|
||||
|
||||
#[error("Internal VM error: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for VmError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
VmError::ArithmeticError(alloc::format!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = core::result::Result<T, VmError>;
|
||||
591
src/rvm/vm/execution.rs
Normal file
591
src/rvm/vm/execution.rs
Normal file
@@ -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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value>) -> Result<Value> {
|
||||
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<Value> {
|
||||
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<bool> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
179
src/rvm/vm/execution_model.rs
Normal file
179
src/rvm/vm/execution_model.rs
Normal file
@@ -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<Value>,
|
||||
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<Value>,
|
||||
pub(super) saved_loop_stack: Vec<LoopContext>,
|
||||
pub(super) saved_comprehension_stack: Vec<ComprehensionContext>,
|
||||
}
|
||||
|
||||
/// Explicit execution stack replacing the Rust call stack
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(super) struct ExecutionStack {
|
||||
frames: Vec<ExecutionFrame>,
|
||||
}
|
||||
|
||||
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<ExecutionFrame> {
|
||||
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<usize>;
|
||||
74
src/rvm/vm/functions.rs
Normal file
74
src/rvm/vm/functions.rs
Normal file
@@ -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<crate::ast::Ref<crate::ast::Expr>> = 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(())
|
||||
}
|
||||
}
|
||||
661
src/rvm/vm/loops.rs
Normal file
661
src/rvm/vm/loops.rs
Normal file
@@ -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<bool> {
|
||||
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<bool> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
325
src/rvm/vm/machine.rs
Normal file
325
src/rvm/vm/machine.rs
Normal file
@@ -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<Value>,
|
||||
|
||||
/// Program counter
|
||||
pub(super) pc: usize,
|
||||
|
||||
/// The compiled program containing instructions, literals, and metadata
|
||||
pub(super) program: Arc<Program>,
|
||||
|
||||
/// Reference to the compiled policy for default rule access
|
||||
pub(super) compiled_policy: Option<CompiledPolicy>,
|
||||
|
||||
/// 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<LoopContext>,
|
||||
|
||||
/// Call rule execution stack for managing nested rule calls
|
||||
pub(super) call_rule_stack: Vec<CallRuleContext>,
|
||||
|
||||
/// Register stack for isolated register spaces during rule calls
|
||||
pub(super) register_stack: Vec<Vec<Value>>,
|
||||
|
||||
/// 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<ComprehensionContext>,
|
||||
|
||||
/// 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<Vec<Value>>,
|
||||
|
||||
/// 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<Value, VecDeque<Value>>,
|
||||
|
||||
/// 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<Program>) {
|
||||
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<String> {
|
||||
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<Value> {
|
||||
&self.registers
|
||||
}
|
||||
|
||||
pub fn get_program(&self) -> &Arc<Program> {
|
||||
&self.program
|
||||
}
|
||||
|
||||
pub fn get_call_stack(&self) -> &Vec<CallRuleContext> {
|
||||
&self.call_rule_stack
|
||||
}
|
||||
|
||||
pub fn get_loop_stack(&self) -> &Vec<LoopContext> {
|
||||
&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<I, J>(&mut self, responses: I)
|
||||
where
|
||||
I: IntoIterator<Item = (Value, J)>,
|
||||
J: IntoIterator<Item = Value>,
|
||||
{
|
||||
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<Value> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/rvm/vm/mod.rs
Normal file
23
src/rvm/vm/mod.rs
Normal file
@@ -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;
|
||||
627
src/rvm/vm/rules.rs
Normal file
627
src/rvm/vm/rules.rs
Normal file
@@ -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<u32>],
|
||||
rule_info: &RuleInfo,
|
||||
function_call_params: Option<&FunctionCallParams>,
|
||||
) -> Result<(Value, bool)> {
|
||||
let mut first_successful_result: Option<Value> = 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<Option<usize>> {
|
||||
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<Option<usize>> {
|
||||
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<Option<usize>> {
|
||||
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<Option<usize>> {
|
||||
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<Option<usize>> {
|
||||
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<Value> {
|
||||
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<Option<usize>> {
|
||||
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<Option<usize>> {
|
||||
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<RuleInfo> {
|
||||
let idx = rule_index as usize;
|
||||
self.program
|
||||
.rule_infos
|
||||
.get(idx)
|
||||
.cloned()
|
||||
.ok_or(VmError::RuleInfoMissing { index: rule_index })
|
||||
}
|
||||
}
|
||||
107
src/rvm/vm/state.rs
Normal file
107
src/rvm/vm/state.rs
Normal file
@@ -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<Value> {
|
||||
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<Value>) {
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
293
src/rvm/vm/virtual_data.rs
Normal file
293
src/rvm/vm/virtual_data.rs
Normal file
@@ -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<Value> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
mod interpreter;
|
||||
pub mod interpreter;
|
||||
mod scheduler;
|
||||
|
||||
17
src/value.rs
17
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<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Value::deserialize(deserializer)
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
|
||||
78
tests/rvm/vm/README.md
Normal file
78
tests/rvm/vm/README.md
Normal file
@@ -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_<suite>_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.
|
||||
168
tests/rvm/vm/suites/arithmetic_operations.yaml
Normal file
168
tests/rvm/vm/suites/arithmetic_operations.yaml
Normal file
@@ -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
|
||||
47
tests/rvm/vm/suites/assertions.yaml
Normal file
47
tests/rvm/vm/suites/assertions.yaml
Normal file
@@ -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
|
||||
32
tests/rvm/vm/suites/basic_instructions.yaml
Normal file
32
tests/rvm/vm/suites/basic_instructions.yaml
Normal file
@@ -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
|
||||
44
tests/rvm/vm/suites/boolean_literals.yaml
Normal file
44
tests/rvm/vm/suites/boolean_literals.yaml
Normal file
@@ -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
|
||||
524
tests/rvm/vm/suites/builtin_functions.yaml
Normal file
524
tests/rvm/vm/suites/builtin_functions.yaml
Normal file
@@ -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
|
||||
76
tests/rvm/vm/suites/call_rule.yaml
Normal file
76
tests/rvm/vm/suites/call_rule.yaml
Normal file
@@ -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"
|
||||
111
tests/rvm/vm/suites/comparison_operations.yaml
Normal file
111
tests/rvm/vm/suites/comparison_operations.yaml
Normal file
@@ -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
|
||||
230
tests/rvm/vm/suites/complex.yaml
Normal file
230
tests/rvm/vm/suites/complex.yaml
Normal file
@@ -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
|
||||
80
tests/rvm/vm/suites/constructed_collections.yaml
Normal file
80
tests/rvm/vm/suites/constructed_collections.yaml
Normal file
@@ -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"
|
||||
115
tests/rvm/vm/suites/control_flow.yaml
Normal file
115
tests/rvm/vm/suites/control_flow.yaml
Normal file
@@ -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
|
||||
232
tests/rvm/vm/suites/core_semantics.yaml
Normal file
232
tests/rvm/vm/suites/core_semantics.yaml
Normal file
@@ -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"]
|
||||
76
tests/rvm/vm/suites/data_structures.yaml
Normal file
76
tests/rvm/vm/suites/data_structures.yaml
Normal file
@@ -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
|
||||
278
tests/rvm/vm/suites/deep_nesting.yaml
Normal file
278
tests/rvm/vm/suites/deep_nesting.yaml
Normal file
@@ -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
|
||||
277
tests/rvm/vm/suites/default_rules.yaml
Normal file
277
tests/rvm/vm/suites/default_rules.yaml
Normal file
@@ -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
|
||||
41
tests/rvm/vm/suites/destructuring_rules.yaml
Normal file
41
tests/rvm/vm/suites/destructuring_rules.yaml
Normal file
@@ -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"
|
||||
371
tests/rvm/vm/suites/function_calls.yaml
Normal file
371
tests/rvm/vm/suites/function_calls.yaml
Normal file
@@ -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"
|
||||
14
tests/rvm/vm/suites/halt.yaml
Normal file
14
tests/rvm/vm/suites/halt.yaml
Normal file
@@ -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
|
||||
137
tests/rvm/vm/suites/host_await.yaml
Normal file
137
tests/rvm/vm/suites/host_await.yaml
Normal file
@@ -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"]
|
||||
28
tests/rvm/vm/suites/host_await_failures.yaml
Normal file
28
tests/rvm/vm/suites/host_await_failures.yaml
Normal file
@@ -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"
|
||||
241
tests/rvm/vm/suites/indexed_access.yaml
Normal file
241
tests/rvm/vm/suites/indexed_access.yaml
Normal file
@@ -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"
|
||||
432
tests/rvm/vm/suites/integration_scenarios.yaml
Normal file
432
tests/rvm/vm/suites/integration_scenarios.yaml
Normal file
@@ -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
|
||||
351
tests/rvm/vm/suites/interpreter_operator_compatibility.yaml
Normal file
351
tests/rvm/vm/suites/interpreter_operator_compatibility.yaml
Normal file
@@ -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"
|
||||
61
tests/rvm/vm/suites/invalid_collection_ops.yaml
Normal file
61
tests/rvm/vm/suites/invalid_collection_ops.yaml
Normal file
@@ -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
|
||||
250
tests/rvm/vm/suites/load_data_input.yaml
Normal file
250
tests/rvm/vm/suites/load_data_input.yaml
Normal file
@@ -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"
|
||||
43
tests/rvm/vm/suites/loop_invalid_iteration.yaml
Normal file
43
tests/rvm/vm/suites/loop_invalid_iteration.yaml
Normal file
@@ -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"
|
||||
160
tests/rvm/vm/suites/loops/array_comprehensions.yaml
Normal file
160
tests/rvm/vm/suites/loops/array_comprehensions.yaml
Normal file
@@ -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]
|
||||
295
tests/rvm/vm/suites/loops/empty.yaml
Normal file
295
tests/rvm/vm/suites/loops/empty.yaml
Normal file
@@ -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: []
|
||||
174
tests/rvm/vm/suites/loops/existential.yaml
Normal file
174
tests/rvm/vm/suites/loops/existential.yaml
Normal file
@@ -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
|
||||
|
||||
223
tests/rvm/vm/suites/loops/loop_comprehension_interactions.yaml
Normal file
223
tests/rvm/vm/suites/loops/loop_comprehension_interactions.yaml
Normal file
@@ -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}
|
||||
183
tests/rvm/vm/suites/loops/nested.yaml
Normal file
183
tests/rvm/vm/suites/loops/nested.yaml
Normal file
@@ -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]
|
||||
7
tests/rvm/vm/suites/loops/nested_fixed.yaml
Normal file
7
tests/rvm/vm/suites/loops/nested_fixed.yaml
Normal file
@@ -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: []
|
||||
192
tests/rvm/vm/suites/loops/object_comprehensions.yaml
Normal file
192
tests/rvm/vm/suites/loops/object_comprehensions.yaml
Normal file
@@ -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}
|
||||
139
tests/rvm/vm/suites/loops/set_comprehensions.yaml
Normal file
139
tests/rvm/vm/suites/loops/set_comprehensions.yaml
Normal file
@@ -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]
|
||||
143
tests/rvm/vm/suites/loops/universal.yaml
Normal file
143
tests/rvm/vm/suites/loops/universal.yaml
Normal file
@@ -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
|
||||
376
tests/rvm/vm/suites/null_undefined_handling.yaml
Normal file
376
tests/rvm/vm/suites/null_undefined_handling.yaml
Normal file
@@ -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"
|
||||
276
tests/rvm/vm/suites/object_operations.yaml
Normal file
276
tests/rvm/vm/suites/object_operations.yaml
Normal file
@@ -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]}
|
||||
165
tests/rvm/vm/suites/predefined.yaml
Normal file
165
tests/rvm/vm/suites/predefined.yaml
Normal file
@@ -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"
|
||||
217
tests/rvm/vm/suites/resource_limits.yaml
Normal file
217
tests/rvm/vm/suites/resource_limits.yaml
Normal file
@@ -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"
|
||||
423
tests/rvm/vm/suites/serialization.yaml
Normal file
423
tests/rvm/vm/suites/serialization.yaml
Normal file
@@ -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]}]}]
|
||||
284
tests/rvm/vm/suites/set_operations.yaml
Normal file
284
tests/rvm/vm/suites/set_operations.yaml
Normal file
@@ -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
|
||||
224
tests/rvm/vm/suites/type_errors.yaml
Normal file
224
tests/rvm/vm/suites/type_errors.yaml
Normal file
@@ -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"
|
||||
274
tests/rvm/vm/suites/virtual_data_lookup.yaml
Normal file
274
tests/rvm/vm/suites/virtual_data_lookup.yaml
Normal file
@@ -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"
|
||||
Reference in New Issue
Block a user